Почему bash - фиговый скриптовый язык

Блог Всячепуза
 
Плохо, что bash-овый скрипт не может использовать API движка, который этот скрипт гоняет. Вот JavaScript в этом плане гораздо лучше. Например, как вызвать python-овые функции portage из eclass-а?

Как я разбирался с CONFIG_PROTECT

мне не ясно, как работает CONFIG_PROTECT, читал вот это - https://devmanual.gentoo.org/general-concepts/config-protect/index.html
там написано, что не надо самостоятельно зверски патчить что попало. Но мне очень хочется

Задача: по репозиторию
https://github.com/gentoo/portage/
разобраться как реализован механизм CONFIG_PROTECT в исходных кодах менеджера portage

Rather than installing protected files directly, Portage will install them as ._cfg0000_filename.
это написано здесь - https://devmanual.gentoo.org/general-concepts/config-protect/index.html

я хочу сделать копию конфига так же, как это делает portage
то есть в pkg_postinst делать копию, в которой что-то добавлено
а в pkg_prerm делать копию, в которой оно удалено
если я правильно поименую эти копии конфигов, то наверное пользователь сможет их обновить так же, как он это делает со всеми остальными конфигами
проблема в том, что нигде не описано, как модифицировать конфиги программно, а не патчами. Например как узнать, какое имя получится у новой копии конфига?

Как проверить

Функции с параметрами и значениями в portage есть
Например на странице
https://devmanual.gentoo.org/ebuild-writing/eapi/
написано
The in_iuse function returns true if the given parameter is available in the ebuilds USE.

CONFIG_PROTECT

Any directory which is listed in CONFIG_PROTECT (and any subdirectories thereof), except for any which are listed in CONFIG_PROTECT_MASK (and subdirectories) are automatically 'protected' by Portage when copying an image from DESTDIR to ROOT.

Надо посмотреть, найти фрагмент, где происходит это копирование из DESTDIR в ROOT. Учитывается ли там CONFIG_PROTECT_MASK ?

CONFIG_PROTECT_MASK

То есть если я хочу некий файл обновить автоматически, я при установке одного конкретного пакета могу добавить путь до файла конфигурации в CONFIG_PROTECT_MASK перед emerge

cnf/make.globals#L106
CONFIG_PROTECT_MASK="/etc/env.d"

Вопрос - вызывается ли что-то подобное etc-update и dispatch-conf автоматически для файлов упомянутых в CONFIG_PROTECT_MASK ? Сомневаюсь...
Выводится уведомление, что надо обновить файлы (без учета CONFIG_PROTECT_MASK), а при обновлении файлы из CONFIG_PROTECT_MASK обновляются автоматически.

etc-update

bin/etc-update#L147-L155
			local mpath
			for mpath in ${CONFIG_PROTECT_MASK}; do
				mpath="${EROOT%/}${mpath}"
				if [[ "${rpath}" == "${mpath}"* ]] ; then
					${QUIET} || echo "Updating masked file: ${live_file}"
					mv "${cfg_file}" "${live_file}"
					continue 2
				fi
			done

dispatch-conf

bin/dispatch-conf#L169-L177
 	#
        # Remove new configs identical to current
        #                  and
        # Auto-replace configs a) whose differences are simply CVS interpolations,
        #                  or  b) whose differences are simply ws or comments,
        #                  or  c) in paths now unprotected by CONFIG_PROTECT_MASK,
        #

        def f (conf):

Что ищется в исходниках

pym/_emerge/post_emerge.py#L18
from .chk_updated_cfg_files import chk_updated_cfg_files
pym/_emerge/post_emerge.py#L95-L96
config_protect = portage.util.shlex_split(
	settings.get("CONFIG_PROTECT", ""))
pym/_emerge/post_emerge.py#L149
	chk_updated_cfg_files(settings['EROOT'], config_protect)
pym/_emerge/chk_updated_cfg_files.py
эта функция вызывает portage.util.find_updated_config_files(target_root, config_protect)
смотрит, какие из них попадают в список CONFIG_PROTECT
и выводит список файлов, которые были изменены, а потом выводит инструкции для пользователя (читать man как обновлять файлы)
pym/portage/util/__init__.py#L1750-L1758
def find_updated_config_files(target_root, config_protect):
	"""
	Return a tuple of configuration files that needs to be updated.
	The tuple contains lists organized like this:
		[protected_dir, file_list]
	If the protected config isn't a protected_dir but a procted_file, list is:
		[protected_file, None]
	If no configuration files needs to be updated, None is returned
	"""
pym/portage/util/__init__.py#L1789
	"find '%s' -maxdepth 1 -name '._cfg????_%s'"

Как нужно назвать файл-копию

Это знает portage. Может он сам и должен такую копию создавать, а мне нужно только скопировать в нужную директорию?
Копирование какой-то конкретной версии конфигурационного файла не будет работать в случае установки бинарного пакета.

Я предалгаю создавать
специальным образом поименованную копию так же, как это делает portage из шагов после инсталляции и
другую копию со стёртыми строками перед удалением пакета

для этого надо разобраться, какой код в portage такие копии создаёт,
это позволит понять, можно ли этим кодом воспользоваться из eclass

В генте ведь какая логика? Сначала надо всё прочитать и только потом задавать вопрос, потому что мерзкие твари считают себя самыми умными.
https://wiki.gentoo.org/wiki/CONFIG_PROTECT
https://wiki.gentoo.org/wiki/Handbook:AMD64/Working/EnvVar
https://devmanual.gentoo.org/general-concepts/config-protect/index.html

emerge --config

--config
Run package specific actions needed to be executed after the emerge process has completed. This usually entails configuration file setup or other similar setups that the user may wish to run.
https://dev.gentoo.org/~zmedico/portage/doc/man/emerge.1.html
запускает pkg_config

Сухой остаток

Нужно внимательно изучить весь питоновый код, работающий с CONFIG_PROTECT и переписать его логику на bash, после чего поместить в eclass
изучать внимательно не обязательно, можно сделать тяп-ляп с приемлемым качеством, потом пипл поплюётся и допилит как надо.

Второй раунд разборок

portageq

Это такая вспомогательная утилита, которой я ни разу не пользовался за много лет:
https://wiki.gentoo.org/wiki/Portageq
https://github.com/gentoo/portage/blob/9590cb4bf4140e3fc5610b1ea0e290b2df93c24a/bin/portageq#L346-L386
def is_protected(argv):
expected 2 parameters: root, filename
Возвращает 0 или 1, использует функции portage.util.shlex_split, ConfigProtect и .isprotected(f)
в документации так про неё и написано:
docstrings['is_protected'] = """ 
	Given a single filename, return code 0 if it's protected, 1 otherwise.
	The filename must begin with .
	"""
is_protected.__doc__ = docstrings['is_protected']

можно из командной строки вызвать, передать параметр $EROOT и имя одного файла и команда вернёт - защищённый этот файл или нет
можно из командной строки вызвать, передать параметр $EROOT и она отфильтрует из всех файлов те файлы, которые CONFIG_PROTECTED
filter_protected <eroot>
      Read filenames from stdin and write them to stdout if they are protected.
      All filenames are delimited by \n and must begin with .

Вопрос - используется ли portageq в каком-либо из билдов?
there is an utility "portageq" which have 2 options - to check one file (is_protected), and to check list of files (filter_protected), since this is command line tool, it can be called from bash script.
This allows to call code of portage for determine protected files from ebuild or eclass

https://github.com/gentoo/portage/blob/b5365341dad167e314023df95d2c5e0f955962f0/pym/portage/util/__init__.py#L1590
class ConfigProtect(object)
https://github.com/gentoo/portage/blob/b5365341dad167e314023df95d2c5e0f955962f0/pym/portage/util/__init__.py#L1639
def isprotected(self, obj):
https://github.com/gentoo/portage/blob/b5365341dad167e314023df95d2c5e0f955962f0/pym/portage/util/__init__.py#L1675
def new_protect_filename(mydest, newmd5=None, force=False):
	"""Resolves a config-protect filename for merging, optionally
	using the last filename if the md5 matches. If force is True,
	then a new filename will be generated even if mydest does not
	exist yet.
	(dest,md5) ==> 'string'            --- path_to_target_filename
	(dest)     ==> ('next', 'highest') --- next_target and most-recent_target
	"""

Как бы вызвать питоновую функцию из bash?

http://stackoverflow.com/questions/8567526/calling-a-python-function-from-a-shell-script
python -c 'import test; print test.get_foo()'
The -c option simply asks Python to execute some Python commands.

In order to store the result in a variable, you can therefore do:

RESULT_FOO=`python -c 'import test; print test.get_foo()'`

or, equivalently

RESULT=$(python -c 'import test; print test.get_foo()')

since backticks and $(…) evaluate a command and replace it by its output.



stackoverflow says it should be something like
RES=$(python -c 'from ??? import ???; new_protect_filename( (???))' )

Пути поиска модулей указаны в переменной sys.path.
В него включены текущая директория (то есть модуль можно оставить в папке с основной программой), а также директории, в которых установлен python.

при импортировании модуля его код выполняется полностью, то есть, если программа что-то печатает, то при её импортировании это будет напечатано. Этого можно избежать, если проверять, запущен ли скрипт как программа, или импортирован. Это можно сделать с помощью переменной __name__, которая определена в любой программе, и равна "__main__", если скрипт запущен в качестве главной программы, и имя, если он импортирован.
python searches packages/modules to import in sys.path which is populated from different sources including a current directory, PYTHONPATH env variable and few other sources

Как передать параметр

http://stackoverflow.com/questions/6719549/passing-bash-variables-to-python-script
you can just pass parameters directly on the command line. Any options passed to Python after the -c command get loaded into the sys.argv array
I think you can just import portage as a package because it lives in your system's site-packages

You can use os.getenv to access environment variables from Python
test.printfoo(os.getenv('foo'))

Как положить вывод команды в переменную

https://devmanual.gentoo.org/ebuild-writing/error-handling/index.html#die-and-subshells
https://www.gnu.org/software/bash/manual/bash.html#Command-Substitution
command > tempfile || die; read myvar<tempfile
В баше даже есть команда для получения имени временного файла. А у портажа есть ${T}
https://www.gnu.org/software/bash/manual/bash.html#Compound-Commands
почему я думаю, что () и $() не то же самое. Потому что кроме () бывает {}, а с ${} уже не получится вызвать внешнюю команду. То есть {} и ${} не то же самое. Тогда нет повода думать так и про () и $()

http://tldp.org/LDP/abs/html/commandsub.html
"Command substitution invokes a subshell."

$ x=$(f(){ echo a;return 0; };f; ) || echo fail
$ x=$(f(){ echo a;return 1; };f; ) || echo fail
fail
man bash / \(list\)
The return status is the exit status of list.
May I use MYVAR=$(command) || die "command failed" or this should be done in some other way?

Теперь надо собраться с мыслями и написать if then else, в котором проверить является ли файл /etc/shells защищаемым.
if portageq is_protected "${EROOT}" "/etc/shells"; then
else
fi

if portageq is_protected / "/etc/shells"; then echo "protected"; else echo "unprotected"; fi
Combine an ANSI C-quoted string ($'...') with a here-string (<<<...):
#!/bin/bash

# Test whether python exit status propagates.

function die () {
    echo "$1"
    exit 1
}

arg=/etc/shells
python <<EOF

import portage

print(portage.new_protect_filename("${arg}"))
EOF
exit_status=$?
if [ $exit_status -ne 0 ]; then
    die "Error: Python protect filename failed!"
fi
https://gist.githubusercontent.com/VsyachePuz/47570ad527044982efb00f7b5d551467/raw/82af53f99acef2974eda7a59885829be8dcf0538/stdin