Sh (от англ. shell) является обязательным командным интерпретатором для UNIX-совместимых систем по стандарту POSIX. Однако по возможностям он ограничен, поэтому зачастую вместо него используются более богатые возможностями командные интепретаторы, такие как Bash или Ksh. Ksh обычно используется в операционных системах семейства BSD, в то время как Bash — в операционных системах семейства Linux. Командные интерпретаторы облегчают решение мелких зачач, связанных с работой с процессами и файлами. В данной статье будут рассматриваться операционные системы Linux, поэтому речь пойдёт о Bash.
Python, в свою очередь, является полноценным интерпретируемым языком программирования, также он нередко используется для написания скриптов или решения мелких прикладных задач. Современную UNIX-подобную систему сложно представить как без sh, так и без Python, если только это не устройство с минималистичной ОС вроде маршрутизатора. Например, в Ubuntu Oracular пакет python3 удалить не получится хотя бы потому, что от него зависит пакет grub-common, от которого, в свою очередь зависят пакеты grub2-common и, соответственно, grub-pc, то есть непосредственно загрузчик операционной системы. Таким образом, Python 3 можно смело использовать как замену Bash в случае необходимости.
При решении различных задач на уровне ОС или файловой системы может возникнуть вопрос, а какой же из языков, Bash или Python выгодно использовать в том или ином случае? И тут всё будет зависеть от решаемой задачи. Bash выгоден, когда нужно быстро решить каку‑либо простую задачу, связанную с управлением процессами, поиском или изменением файлов. В случае же усложнения логики код на Bash становится слишком громозким и трудночитаемым (хотя читабельность в первую очередь будет зависеть от самого программиста). Можно, конечно код разбивать на скрипты и функции, делать sh-библиотеки, подключаемые через команду source, но модульными тестами это уже сложно будет покрывать.
Предисловие
Для кого эта статья? Для тех, кто увлекается системным администрированием, знаком с одним из двух языков и хочет разобраться со вторым. Либо же для тех, кто хочет познакомиться с некоторыми особенностями Bash и Python, которые он раньше мог не знать. Для понимания материала требуются базовые навыки работы с командной строкой и знакомство с основами программирования.
Для полной картины, в том числе и по читабльности кода, в статье будет приведено сравнение по возможностям отладки, по синтаксису и по тем или иным случаям использования. Будут приводиться аналогичные друг другу примеры на обоих языка. В коде на Python будут иногда встречаться запятые в конце перечислений, это не ошибки, — такой стиль является хорошей практикой, поскольку при добавлении новых элементов в перечисление позволяет избежать пометки последнего элемента как изменённого.
В статье будет рассматриваться Bash как минимум версии 3.0 и Python как минимум версии 3.7.
Отладка скриптов
Оба языка являются интерпретируемыми, это означает, что в момент исполнения скриптов, интерпретатор знает достаточно много о текущем состоянии исполнения.
Отладка в Bash
Отладка через xtrace
Bash поддерживает опцию xtrace
(-x
), которую можно задать как в командной строке при запуске интерпретатора, так и внутри самого скрипта:
#!/bin/bash
# Указываем, куда необходимо писать логи, открываем файл на запись:
exec 3>/путь/к/файлу/логов
BASH_XTRACEFD=3 # в какой файловый дескриптор выводить отладочную информацию
set -x # включаем отладку
# ... отлаживаемы код ...
set +x # выключаем отладку
Такие логи, например, можно писать и в журнал systemd, если реализуется какой-либо простой сервис:
#!/bin/bash
# Указываем, куда необходимо писать логи:
exec 3> >(systemd-cat --priority=debug)
BASH_XTRACEFD=3 # в какой поток выводить отладочную информацию
set -x # включаем отладку
# ... отлаживаемы код ...
set +x # выключаем отладку
Отладка в Bash будет показывать, какие команды запускаются и с какими аргументами. Если требуется получить текущие значения переменных или код исполняемых функций, то это можно сделать командой set
без аргументов. Однако поскольку вывод команды может быть достаточно большой, то set
под ходит для ручной отладки, нежели для логирования по событиям.
Отладка через trap
Другим способом отладки является установка обработчиков на запуск команд с помощь команды trap
на специальную «ловушку» DEBUG
. Запускаемые команды могут быть получены через встроенную переменную BASH_COMMAND
. Однако код возврата по этому обработчику получить не получится, поскольку он запускается до вызова самой комнады.
trap 'echo "+ ${BASH_COMMAND}"' DEBUG
Но более полезным будет перехват ошибок и вывод команды и номера строки, на которых ошибка произошла. Для наследования этого перехвата функциями понадобится ещё установить опцию functrace
:
set -o functrace
trap 'echo "+ строка ${LINENO}: ${BASH_COMMAND} -> $?"' ERR
# Тестируем:
ls "${PWD}"
ls unknown_file
Отладка в Python
Отладка через pdb
В Python богатые средства отладки и логирования. По части отладки в Python есть модуль pdb. Можно запускать скрипт с включенной из консоли отладкой, в таком случае при исключительных ситуациях будет включаться режим отладки:
python3 -m pdb my_script.py
Непосредственно в коде можно устанавливать точки останова с помощью встроенной функции breakpoint()
.
#!/usr/bin/python3
import os
breakpoint()
# Теперь можно попробовать, например, команду source os:
# (Pdb) source os
Сам язык является объектно‑ориентированным, в нём всё является объектами. Посмотреть, какие методы есть у объекта, можно с помощью команды dir()
. Так, через dir(1)
можно узнать, какие методы есть у объекта 1
. Пример вызова одного из таких методов: (1).bit_length()
. Во многих случаях это помогает разобраться с возникающими вопросами даже без необходимости чтения документации. В режиме отладки также можно использовать команды dir()
для получения информации об объектах и print()
для получения значений переменных.
Логирование через модуль logging
Python предоставляет модуль logging, который позволяет записывать в лог отладочную информацию с указанием уровней логирования и источника логов. В целом логирование выглядит примерно так:
import logging
logging.basicConfig(
filename = 'myscript.log',
level = logging.DEBUG, # выводить уровни DEBUG, INFO, WARNING, ERROR и CRITICAL
)
logger = logging.getLogger('MyApp')
logger.debug('Some debug information')
logger.error('Some error')
Сравнение семантики Bash и Python
Переменные и типы данных
Примитивные типы данных
В Bash все переменные строковые, но строковые переменные можно использовать и как числа. Для получения результата арифметических вычислений применяется синтаксическая конструкция $(( выражение ))
.
str_var='some_value' # строка, массив символов
int_var=1234 # строка "1234", но можно использовать в вычислениях
int_var=$(( 1 + (int_var - 44) / 111 - 77 )) # строка: "-66"
str_var = 'some_value' # класс str
int_var = 1234 # класс int
int_var = 1 + (int_var - 44) // 111 - 77 # -66, класс int
Вещественные же числа в Bash не поддерживаются. И это логично, ведь если потребовалось использовать вещественные числа в скриптах командной строки, то явно делается что‑то не на том уровне или не на том языке программирования. Тем не менее, вещественные числа поддерживаются в Ksh.
Форматирование строк
И Bash и Python поддерживают подстановку значения переменных в форматированные строки. В Bash форматируемыми строками являются строки, заключённые в кавычки, а в Python — строки с префиксом f
.
Также оба языка поддерживают C-подобный стиль вывода форматированных строк. В Bash таким образом можно даже форматировать вещественные числа, хотя сам язык их и не поддерживает (разделитель десятичной части определяется локалью).
var1='Some string'
var2=0,5
echo "Переменная 1: ${var1}, переменная 2: ${var2}"
# Переменная 1: Some string, переменная 2: 0,5
# Без текущей локали
LANG=C \
printf 'Строка: %s, число: %d, вещественное число: %f.\n' \
'str' '1234' '0.1'
# С текущей локалью
printf 'Строка: %s, число: %d, вещественное число: %f.\n' \
'str' '1234' '0,1'
# Строка: str, число: 1234, вещественное число: 0,100000.
var1 = 'Somstr_var = 'some_value'
int_var = 1234e string'
var2 = 0.5
print(f"Переменная 1: {var1}, переменная 2: {var2}")
# Переменная 1: Some string, переменная 2: 0.5
# Без текущей локали:
print('Строка: %s, число: %d, вещественное число: %f.'
% ('str', 1234, 0.1))
# Строка: str, число: 1234, вещественное число: 0.100000.
# С текущей локалью:
import locale
locale.setlocale('') # применяем текущую локаль
print(locale.format_string('Строка: %s, число: %d, вещественное число: %f.',
('str', 1234, 0.1)))
# Строка: str, число: 1234, вещественное число: 0,100000.
Можно заметить отличие в плане локали — в Python функция print()
игнорирует локаль. Если требуется вывод значений с учётом локали, то необходимо использовать функцию locale.format_string()
.
Массивы
В Bash массивы — это по сути текст, разделённый пробелами (по умолчанию). При этом синтаксис очень специфичен, например, для копирования массива (через @
) получение всех его элементов надо заключать в кавычки, иначе любые пробелы в самих элементах приведут к разделению элемента на части. Но в целом работа с массивами между языками схожа в простых случаях:
arr=( 'First item' 'Second item' 'Third item' )
echo "${arr[0]}" "${arr[1]}" "${arr[2]}"
arr_copy="${arr[@]}" # копирование массива, кавычки обязательны
arr[0]=1
arr[1]=2
arr[2]=3
echo "${arr[@]}"
echo "${arr_copy[0]}" "${arr_copy[1]}" "${arr_copy[2]}"
arr = [ 'First', 'Second', 'Third' ]
print(arr[0], arr[1], arr[2])
arr_copy = arr.copy() # но можно делать и как в Bash: [ *arr ]
arr[0] = 1
arr[1] = 2
arr[2] = 3
print(*arr)
print(arr_copy[0], arr_copy[1], arr_copy[2])
Оператор *
в Python выполняет распаковку списков, словарей, итераторов и т. п. То есть элементы массива как будто перечисляются через запятую в качестве аргументов.
Ассоциативные массивы
Ассоциативные массивы Bash тоже поддерживает (в отличие от Sh), но возможности по работе с ними ограничены. В Python же ассоциативные массивы называются словарями, и язык предоставляет очень богатые возможности для работы с ними.
declare -A assoc_array=(
[name1]='Значение 1'
[name2]='Значение 2'
[name3]='Значение 3'
)
# Присвоение значения по ключу:
assoc_array['name4']='Значение 4' # присвоение значения
# Поэлементный доступ:
echo "${assoc_array['name1']}" \
"${assoc_array['name2']}" \
"${assoc_array['name3']}" \
"${assoc_array['name4']}"
echo "${!assoc_array[@]}" # вывести все ключи
echo "${assoc_array[@]}" # вывести все значения
# Обход всех элементов
for key in "${!assoc_array[@]}"; do
echo "Key: ${key}"
echo "Value: ${assoc_array[$key]}"
done
assoc_array = {
'name1': 'Значение 1',
'name2': 'Значение 2',
'name3': 'Значение 3',
}
# Присвоение значения по ключу:
assoc_array['name4'] = 'Значение 4'
# Поэлементный доступ
print(
assoc_array['name1'],
assoc_array['name2'],
assoc_array['name3'],
assoc_array['name4']
)
print(*assoc_array) # вывести все ключи
print(*assoc_array.values()) # вывести все значения
for key, value in assoc_array.items():
print(f"Key: {key}")
print(f"Value: {value}")
Подключение модулей
В Bash как таковые модули отсутствуют. Но в нём можно исполнить скрипт в текущем интерпретаторе через команду source
. Фактически, это аналог импортирования модулей, поскольку все функции подключаемого скрипта становятся доступны в пространстве текущего интерпретатора. В Python же есть полноценная поддержка модулей с возможностью их импорта. При этом стандартная библиотека Python содержит большое количество модулей для самых разных сценариев использования. Фактически, то, что в Bash реализуется сторонними утилитами командной строки, в Python может быть доступно в виде модулей стандартной библиотеки (а если нет, то можно установить дополнительные библиотеки).
Подключаем файл mylib.sh с какими-либо функциями:
source mylib.sh
# Посмотрим список доступных функций (вообще всех]):
declare -F
# Подключаем модуль mylib.py или mylib.pyc:
import mylib
# Посмотрим список доступных объектов модуля mylib:
print(dir(mylib))
Ветвления и циклы
Условный оператор
В Bash условия работают по двум принципам: либо в качестве условия подаётся команда, и проверяется её код возврата, либо используются встроенные в Bash двойные квадратные или двойные круглые скобки. При этом в случае кода возврата 0 является истиной (всё хорошо), а в случае двойных круглых скобок всё наоборот, проверяется результат арифметического выражения, где 0 — ложь.
В Python же стандартный для языков программирования подход: False
, 0
, ''
, []
, set()
, {}
— всё это приравнивается к False
. Непустые ненулевые значения — к True
.
if [[ "${PWD}" == "${HOME}" ]]; then
echo 'Текущий каталог: ~'
elif [[ "${PWD}" == "${HOME}"* ]]; then
echo "Текущий каталог: ~${PWD#${HOME}}"
else
echo "Текущий каталог: ${PWD}"
fi
if (( UID < 1000 )); then
echo "Вы вошли под системным пользователем. Пожалуйста, войдите под собой."
fi
import os
curr_dir = os.environ['PWD']
home_dir = os.environ['HOME']
if curr_dir == home_dir:
print('Текущий каталог: ~')
elif curr_dir.startswith(home_dir):
print('Текущий каталог: ~' + curr_dir[len(home_dir):])
else:
print(f"Текущий каталог: {curr_dir}")
if os.environ['UID'] < 1000:
print('Вы вошли под системным пользователем. Пожалуйста, войдите под собой.')
Циклы
Оба языка поддерживают циклы for
и while
.
Цикл с обходом элементов
В обоих языках цикл for
поддерживает обход элементов через оператор in
. В Bash обоходятся элементы массива или элементы строки, разделённые разделителями, записанными в переменной IFS
(по умолчанию пробел, табуляция и перевод строки). В Python оператор in
позволяет обходить любые итерируемые объекты, например списки, множества, кортежи и словари и более безопасен в работе.
# Перекодирование текстовых файлов из CP1251 в UTF-8
for filename in *.txt; do
tmp_file=`mktemp`
iconv -f CP1251 -t UTF-8 "${filename}" -o "${tmp_file}"
mv "${tmp_file}" "${filename}"
done
import glob
from pathlib import Path
# Перекодирование текстовых файлов из CP1251 в UTF-8
for filename in glob.glob('*.txt'):
file = Path(filename)
text = file.read_text(encoding='cp1251')
file.write_text(text, encoding='utf8')
Цикл for со счётчиком
Цикл со счётчиком в Bash выглядит непривычно, используется форма для арифметических вычислений ((инициализация; условия; действия после итерации))
.
# Получаем список всех локально прописанных хостов:
mapfile -t lines < <(grep -P -v '(^\s*$|^\s*#)' /etc/hosts)
# Выводим список с нумерацией:
for ((i = 0; i < "${#lines[@]}"; i += 1)); do
echo "$((i + 1)). ${lines[$i]}"
done
from pathlib import Path
import re
def is_host_line(s):
return not re.match(r'(^\s*$|^\s*#)', s)
lines = list(filter(is_host_line, Path('/etc/hosts').read_text().splitlines()))
for i in range(0, len(lines)):
print(f"{i + 1}. {lines[i]}")
Функции
Как и в обычных языках, в Bash поддерживаются функции. По своей сути функции в Bash похожи на отдельные скрипты — им также можно передавать аргументы как обычным скриптам, и они возвращают код возврата. Но, в отличие от Python, они не могут вернуть результат, отличный от кода возврата. Однако можно возвращать текст через поток вывода.
some_function()
{
echo "Script: $0."
echo "Function: ${FUNCNAME}."
echo "Function arguments:"
for arg in "$@"; do
echo "${arg}"
done
return 0
}
some_function Раз Два Три Четыре Пять
echo $? # Код возврата
import inspect
def some_function_is_ok(*args):
try: # Если вдруг запустили из интерпретатора
script_name = __file__
except:
script_name = ''
print('Script: ' + script_name)
print('Function: ' + inspect.getframeinfo(inspect.currentframe()).function)
print('Function arguments:')
print(*args, sep='\n')
return True
result = some_function_is_ok('Раз', 'Два', 'Три', 'Четыре', 'Пять')
print(result) # True
Потоки ввода, вывода и ошибок
Поток ввода служит для получения информации процессом, а в поток вывода информация выводится. Почему потоки, а не обычные переменные? Потому что в потоках информация может обрабатываться по мере её появления. Поскольку информация из потока вывода может проходить дальнейшую обработку, сообщения об ошибках эту информацию могут сломать. Поэтому ошибки выводятся в отдельный поток ошибок. Впрочем, при запуске команды в интерактивном режиме эти потоки перемешиваются. Поскольку это потоки, их можно перенаправлять, например, в файл. Или наоборот, считывать файл в поток ввода. В Bash поток ввода имеет номер 0, поток вывода — 1, поток ошибок — 2. Если в операторе перенаправления в файл не указан номер потока, то перенаправляется поток вывода.
Запись в файл
Запись в файл в Bash осуществляется с помощью оператора >
, который перенаправляет вывод команды в указанный после неё файл. В Python писать текстовые файлы можно с помощью модуля pathlib
, либо стандартными средствами, — посредством открытия файла через функцию open()
. Последний вариант сложнее, но хорошо знаком программистам.
# Очиститьтекстовый файл, перенаправив в него вывод пустой строки:
echo -n > some_text_file.txt
# Записать в файл строку, затерев его:
echo 'Строка 1' > some_other_text_file.txt
# Добавить строку в файл строку:
echo 'Строка 2' >> some_other_text_file.txt
from pathlib import Path
# Перезаписываем файл пустой строкой (делаем его пустым):
Path('some_text_file.txt').write_text('')
# Перезаписываем файл строкой:
Path('some_other_text_file.txt').write_text('Строка 1')
# Открываем файл на дозапись (a):
with open('some_other_text_file.txt', 'a') as fd:
print('Строка 2', file=fd)
Запись в файл многострочного текста
Для многострочного текста в Bash есть специальный формат heredoc (произвольная метка после <<<
, повтор которой с новой строки будет означать конец текста), который позволяет перенаправить произвольный текст в поток ввода команды, а уже из команды его можно перенаправить в файл (и тут уже без внешней команды cat
не обойтись). С перенаправлением же содержимого файла в процесс намного проще.
# Перенаправление многострочного текста в файл на дозапись:
cat <<<EOF >> some_other_text_file.txt
Строка 3
Строка 4
Строка 5
EOF
# Перенаправляет содержимое файла в команду cat:
cat < some_other_text_file.txt
# Открываем файл на дозапись (w+):
with open('some_other_text_file.txt', 'w+') as fd:
print("""Строка 3
Строка 4
Строка 5""", file=fd)
# Открываем файл на чтение (r):
with open('some_other_text_file.txt', 'r') as fd:
# Выводим построчно содержимое файла:
for line in fd:
print(line)
# Можно и fd.read(), то тогда файл будет считан в память целиком.
Чтение из файла
В Bash чтение из файла осуществляется через знак <
. В Python можно читать стандартным способом через open()
, а можно и простым — через Path(...).read_text()
:
cat < some_other_text_file.txt
import pathlib
print(Path('some_other_text_file.txt').read_text())
Перенаправление потоков
Перенаправлять потоки можно не только в файл или в процесс, но и в другой поток.
error()
{
# Перенаправляем поток вывода и поток ошибок в поток ошибок (2).
>&2 echo "$@"
}
error 'Произошла ошибка.'
print('Произошла ошибка.', file=sys.stderr)
В простых случаях перенаправление в файл или из файла в Bash выглядит намного понятнее и проще, чем запись в файл или чтение из него в Python. Однако в сложных случаях код на Bash будет менее понятным и более сложным для анализа.
Выполнение внешних команд
Запуск внешних команд в Python более громоздкий, нежели в Bash. Хотя, конечно, есть простые функции subprocess.getoutput()
и subprocess.getstatusoutput()
, но в них теряется преимущество Python в плане передаче каждого отдельного аргумента как элемента списка.
Получение вывода команды
Если из команды требуется просто получить текст и мы уверены, что это всегда будет работать, то это можно сделать следующим образом:
cmd_path="`which ls`" # косые кавычки выполняют команду и возвращают её вывод
echo "${cmd_path}" # вывести путь к команде
import subprocess
cmd_path = subprocess.getoutput("which ls").rstrip('\n')
print(cmd_path) # выводим путь к команде ls
Но само по себе получение вывода акоманды через косые кавычки в Bash будет неправильным, если требуется получить массив строк. В Python subprocess.getoutput()
принимает командную строку, а не массив аргументов, что несёт некоторые риски при подстановке значений. И оба варианта не игнорируют код возврата исполняемой команды.
Запуск же утилиты в Python для получения какого‑либо списка в переменную займёт намного больше кода, нежели в Bash, хотя код в Python будет намного понятнее и проще:
mapfile -t root_files < <(ls /) # помещаем в root_files список файлов из /
echo "${root_files[@]}" # Вывести список файлов
import subprocess
result = subprocess.run(
['ls', '/'], # мы уверены, что такая команда есть
capture_output = True, # получить вывод команды
text = True, # интерпретировать ввод и вывод как текст
)
root_files = result.stdout.splitlines() # получаем строки из вывода
print(*root_files, sep='\n') # выводим по файлу на строку
Получение и обработка кода возврата
С полноценной обработкой ошибок всё ещё сложнее, добавляются проверки, усложняющие код:
root_files="`ls /some/path`" # Запуск команды в косых кавычках
if [[ $? != 0 ]]; then
exit $?
fi
echo "${root_files[@]}" # Вывести список файлов
import subprocess
import sys
result = subprocess.run(
['ls', '/some/path'],
capture_stdout = True, # получить вывод команды
text = True, # интерпретировать ввод и вывод как текст
shell = True, # чтобы получить код возврата, а не исключение, если команды нет
)
if result.returncode != 0:
sys.exit(result.returncode)
root_files = result.stdout.split('\n') # получаем строки из вывода
del root_files[-1] # последняя строка будет пустой из-за \n в конце, удаляем
print(*root_files, sep='\n') # выводим по файлу на строку
Выполнение команды с одним лишь получением кода возврата чуть проще:
any_command any_arg1 any_arg2
exit_code=$? # получаем код возврата предыдущей команды
if [[ $exit_code != 0 ]]; then
exit 1
fi
import subprocess
import sys
result = subprocess.run(
[
'any_command',
'any_arg1',
'any_arg2',
],
shell = True, # чтобы получить код ошибки несуществующего процесса, а не исключение
)
if result.returncode != 0:
sys.exit(1)
Исключения вместо обработки кода возврата
Но всё становится ещё проще, если включен режим выхода из скрипта по любой ошибке. В Python такой подход применяется по умолчанию, ошибки не требуется проверять вручную, функция может выбросить исключение и аварийно завершить работу процесса.
set -o errexit # аварийное завершение по ошибкам команда
set -o pipefail # весь пайплайн завершается с ошибкой, если ошибка внутри пайплайна
critical_command any_arg1 any_arg2
import subprocess
subprocess.run(
[
'critical_command',
'any_arg1',
'any_arg2',
],
check = True, # выбросить исключение при ненулевом коде возврата
)
В отдельных случаях исключения можно перехватить и обработать. В Python это осуществляется через оператор try
. В Bash такие перехваты осуществляются через обычный оператор if
.
set -o errexit # аварийное завершение по ошибкам команда
set -o pipefail # весь пайплайн завершается с ошибкой, если ошибка внутри пайплайна
if any_command any_arg1 any_arg2; then
do_something_else any_arg1 any_arg2
fi
import subprocess
try:
subprocess.run(
[
'critical_command',
'any_arg1',
'any_arg2',
],
check = True, # выбросить исключение при ненулевом коде возврата
)
except:
subprocess.run(
[
'do_something_else',
'any_arg1',
'any_arg2',
],
check = True, # выбросить исключение при ненулевом коде возврата
)
В высокоуровневых языках стараются обработку ошибок через исключения делать. Код получается проще и понятнее, а значит меньше шансов допустить ошибку, да и рецензирование становится дешевле. Хотя иногда такие проверки выглядят более громоздкими, чем обычная проверка кода возврата. Использовать ли такой стиль обработки ошибок во многом зависит от того, будут ли такие проверки на исключения частыми либо же будут в исключительных случаях.
Построение конвейеров
В Bash конвейеры являются обычной практикой и в самом языке есть синтаксис для создания конвейеров. Поскольку Python не является командным интерпретатором, в нём это делается чуть более громозко, через модуль subprocess
.
ls | grep -v '\.txt$' | grep 'build'
import subprocess
p1 = subprocess.Popen(
['ls'],
stdout = subprocess.PIPE, # для передачи вывода в следующую команду
text = True,
)
p2 = subprocess.Popen(
[
'grep',
'-v',
'\\.txt$'
],
stdin = p1.stdout, # создаём конвейер
stdout = subprocess.PIPE, # для передачи вывода в следующую команду
text = True,
)
p3 = subprocess.Popen(
[
'grep',
'build',
],
stdin = p2.stdout, # создаём конвейер
stdout = subprocess.PIPE, # уже для чтения из текущего процесса
text = True,
)
for line in p3.stdout: # читаем построчно по мере поступления данных
print(line, end='') # каждая строка уже оканчивается \n
Конвейеры с параллельной обработкой данных
В Bash конвейеры можно создавать как между командами, так и между командами и блоками интерпретатора. Например, можно перенаправить конвейер в цикл построчного чтения. В Python же обработка данных из параллельно запущенного процесса тоже ведётся простым построчным чтением их потока вывода процесса.
# Получить список файлов, в которых содержится какой-либо текст:
find . -name '*.txt' \
| while read line; do # поочерёдно получаем пути к файлам
if [[ "${line}" == *'text'* ]]; then # вхождение подстроки в строку
echo "${line}"
fi
done
import subprocess
p = subprocess.Popen(
[
'find',
'.',
'-name',
'*.txt'
],
stdout=subprocess.PIPE,
text=True,
)
while True:
line = p.stdout.readline().rstrip('\n') # на конце всегда есть \n
if not line:
break
if 'text' in line: # вхождение подстроки в строку
print(line)
Параллельное исполнение процессов с ожиданием их завершения
В Bash запуск процесса в фоновом режиме поддерживается на уровне синтаксиса языка (оператор &
), при этом можно запускать как отдельные команды в фоне, так и части интерпретатора (например, функции или циклы). Но на таком уровне сложности код зачастую будет более простым и понятным, если он написан на Python, к тому же стандартная библиотека предоставляет возможности, которые на уровне командного интерпретатора реализуются сторонними утилитами, которые необходимо учитывать в качестве зависимостей.
unalias -a # на случай, если кто-то будет копировать прямо в терминал
get_size_by_url()
{
url="$1"
# Размер файла получим из поля Content-Length заголовков ответа на запрос HEAD
curl --head --silent --location "${url}" \
| while read -r line; do
# Ищем размер в заголовках с помощью регулярного выражения
if [[ "${line}" =~ ^Content-Length:[[:space:]]*(.+)[[:space:]]+$ ]]; then
echo -n "${BASH_REMATCH[1]}" ## 1 соответствует первой открывающейся скобке
return 0
fi
done
}
download_range()
{
url="$1"
start=$2
end=$3
output_file="$4"
((curr_size = end - start + 1))
curl \
--silent \
--show-error \
--range "${start}-${end}" \
"${url}" \
--output - \
| dd \
of="${output_file}" \
oflag=seek_bytes \
seek="${start}" \
conv=notrunc
}
download_url()
{
url="$1"
output_file="$2"
((file_size = $(get_size "${url}")))
# Заранее выделяем место на диске под файл:
fallocate -l "${file_size}" "${output_file}"
range_size=10485760 # 10 МиБ
# Делим на части по максимум 100 МиБ:
((ranges_count = (file_size + range_size - 1) / range_size))
declare -a pids ## Будем сохранять все идентификаторы процессов
for ((i = 0; i < ranges_count; i += 1)); do
((start = i * range_size))
((end = (i + 1) * range_size - 1))
if ((end >= file_size)); then
((end = file_size - 1))
fi
# Запускаем загрузку в фоновом режиме:
download_range "${url}" $start $end "${output_file}" &
pids[$i]=$! # запоминаем PID фонового процесса
done
wait "${pids[@]}" # ждём завершения процессов
}
import requests
from multiprocessing import Process
import os
def get_size_by_url(url):
response = requests.head(url)
return int(response.headers['Content-Length'])
def download_range(url, start, end, output_file):
req = requests.get(
url,
headers = { 'Range': 'bytes=' + str(start) + '-' + str(end) },
stream = True,
)
req.raise_for_status()
with open(output_file, 'r+b') as fd:
fd.seek(start)
for block in req.iter_content(4096):
fd.write(block)
def download_url(url, output_file):
file_size = get_size_by_url(url)
range_size = 10485760 # 10 МиБ
ranges_count = (file_size + range_size - 1) // range_size
with open(output_file, 'wb') as fd:
# Выделяем место под файл заранее:
os.posix_fallocate(fd.fileno(), 0, file_size)
processes = []
for i in range(ranges_count):
start = i * range_size
end = start + range_size - 1
if end >= file_size:
end = file_size - 1
# Подготавливаем процесс и запускаем его в фоновом режиме:
process = Process(
target = download_range, # эта функция будет работать в фоне
args = (url, start, end, output_file),
)
process.start()
processes.append(process)
for process in processes:
process.join() # ожидаем завершения каждого процесса
Подстановка процессов
Отдельной темой, которую стоит упомянуть, является подстановка процессов в Bash через конструкцию <(...)
, поскольку не все о ней знают, но она очень облегчает жизнь. Иногда требуется передать командам потоки информации от других процессов, но при этом сами команды могут лишь принимать на вход пути к файлам. Можно было бы перенаправить вывод процессов во временные файлы, но такой код будет громоздким. Поэтому в Bash есть поддержка подстановки процессов. По факту создаётся виртуальный файл в пространстве /dev/fd/
, через который и передаётся информация посредством передачи имени этого файла в необходимую команду в качестве обычного аргумента.
# Ищем общие процессы на двух хостах:
comm \
<(ssh user1@host1 'ps -x --format cmd' | sort) \
<(ssh user2@host2 'ps -x --format cmd' | sort)
from subprocess import check_output
def get_common_lines(lines1, lines2):
i, j = 0, 0
common = []
while i < len(lines1) and j < len(lines2):
while lines2[j] < lines1[i]:
j += 1
if j >= len(lines2):
return common
while lines2[j] > lines1[i]:
i += 1
if i >= len(lines1):
return common
common.append(lines1[i])
i += 1
j += 1
return common
lines1 = check_output(
['ssh', 'user1@host1', 'ps -x --format cmd'],
text = True,
).splitlines()
lines1.sort()
lines2 = check_output(
['ssh', 'user2@host2', 'ps -x --format cmd'],
text = True,
).splitlines()
lines2.sort()
print(*get_common_lines(lines1, lines2), sep='\n')
Переменные окружения
Работа с переменными окружения
Переменные окружения позволяют передавать информацию от родительских процессов к дочерним. В Bash встроена поддержка переменных окружения на уровне языка, но отсутствует какой‑либо ассоциативный массив всех переменных окружения. Получить информацию о них можно лишь через внешнюю команду env
.
# Присвоение значения переменной окружения:
export SOME_ENV_VAR='Some value'
echo "${SOME_ENV_VAR}" # получение значения
env # вывести список переменных окружения с помощью внешней команды
import os
# Присвоение значения переменной окружения:
os.environ['SOME_ENV_VAR'] = 'Some value'
print(os.environ['SOME_ENV_VAR']) # получение значения
print(os.environ) # вывести массив переменных окружения
Задание значения для отдельных процессов
Переменные окружения передаются от родительского процесса к дочерним. Иногда может потребоваться изменить лишь одну переменную окружения. Поскольку Python позиционируется как язык прикладного программирования, то на нём это будет несколько сложнее, в Bash же поддержка такого задания переменных встроена:
# Устанавливаем русскую локализацию для запускаемых приложений
export LANG='ru_RU.UTF-8'
LANG='C' ls --help # а эту команду запустим с английский локазиацией
echo "LANG=${LANG}" # убедимся, что переменные окружения не затронуты
import os
import subprocess
# Присвоение значения переменной окружения:
os.environ['LANG'] = 'ru_RU.UTF-8'
new_env = os.environ.copy()
new_env['LANG'] = 'C'# Присвоение значения переменной окружения:
export SOME_ENV_VAR='Some value'
echo "${SOME_ENV_VAR}" # получение значения
subprocess.run(
['ls', '--help'],
env = new_env,
)
print('LANG=' + os.environ['LANG']) # убедимся, что переменные окружения не затронуты
Выполнение произвольного кода
Выполнять произвольный код в обыденных ситуациях не требуется, но в обоих языках присутствует такая возможность. В Bash это может пригодиться, например, чтобы возвращать изменённые процессом переменные или чтобы вообще возвращать именованные результаты исполнения. В Python же есть два оператора: eval()
и exec()
. Аналогом eval
языка Bash в данном случае является оператор exec()
, поскольку позволяет выполнять список команд, а не только вычислять выражения. Использование eval()
и exec()
является очень плохой практикой в Python, и эти операторы всегда можно заменить чем‑то более подходящим, если только не требуется написать собственный командный интерпретатор на основе Python.
get_user_info()
{
echo "user=`whoami`"
echo "curr_dir=`pwd`"
}
eval $(get_user_info) # исполняем вывод команды
echo "${user}"
echo "${curr_dir}"
import getpass
import os
def user_info_code():
return f"""
user = '{getpass.getuser()}' # очень плохая практика
curr_dir = '{os.getcwd()}' # не делайте так, пожалуйста
"""
exec(user_info_code())
print(user)
print(curr_dir)
# Но возвращать именованные значения вообще
# лучше через классы, namedtuple или словари
from collections import namedtuple
import getpass
import os
UserInfo=namedtuple('UserInfo', ['user', 'curr_dir'])
def get_user_info():
return UserInfo(getpass.getuser(), os.getcwd())
info = get_user_info()
print(info.user)
print(info.curr_dir)
Работа с файловой системой и процессами
Получение и смена текущего каталога
Менять текущий каталог в командной строке обычно требуется, когда что‑то делается вручную. А вот получать текущий каталог может понадобиться и в скриптах, например, если скрипт ли запускаемая программа что‑то делает над файлами в текущем каталоге. По той же причине может понадобиться и менять текущий каталог, если требуется запустить другую программу, которая что‑то выполняет в нём.
current_dir=`pwd` # получить текущий каталог
echo "${current_dir}"
cd /some/path # перейти в каталог
import os
current_dir = os.getcwd() # получить текущий каталог
print(current_dir)
os.chdir('/some/path') # перейти в каталог
Работа с сигналами
В Bash команда kill
является встроенной, собственно, поэтому man kill
будет выдавать справку совсем по другой команде с отличными аргументами. К слову, sudo kill
будет уже вызывать именно утилиту kill
. Но код на Python все же слегка понятнее.
usr1_handler()
{
echo "Получен сигнал USR1"
}
# Назначаем обработчик сигнала SIGUSR1:
trap 'usr1_handler' USR1
# Послать сигнал текущему интерпретатору:
kill -USR1 $$ # $$ — PID родительского интерпретатора
Возможность компиляции
Bash по определению не поддерживает компиляцию своих скриптов, возможно, поэтому всё в нём стремится к минимализму в названиях. Python же хоть и является интерпретируемым, но может быть скомпилирован в платформонезависимый байт‑код, исполняемый виртуальной машиной Python (PVM). Исполнение такого кода позволяет повысить производительность работы скриптов. Обычно файлы байт‑кода имеют расширение .pyc.
Выбор языка в зависимости от задачи
В качестве итога статьи можно cформировать основные постулаты, какой язык в каких случаях лучше использовать.
Bash выгоднее использовать в случаях:
решения простых задач, которые можно быстрее решить с хорошими знаниями языка;
простых сценариев командной строки, где производится работа с процессами, файлами, каталогами или вообще с жесткими дисками и файловой системой;
если создаются обёртки над другими командами (старт командного интерпретатор может быть быстрее, нежели интерпретатора Python);
если по какой‑то причине Python отсутствует в системе.
Python больше подойдёт для случаев:
решения задач, связанных с обработкой текста, математическими вычислениями или реализацией нетривиальных алгоритмов;
если код на Bash будет трудночитаемым и малопонятным;
если требуется покрывать код модульными тестами (модуль
unittest
);если требуется разбор большого набора параметров командной строки с иерархией опций между командами;
если требуется отображение графических диалоговых окон;
если критична производительность именно в работе скрипта (старт в Python может быть медленнее, но исполнять код он может быстрее);
для создания постоянно работающих служб (сервисы systemd).
Рекомендуемая литература
Cooper M., Advanced Bash‑Scripting Guide / M. Cooper. — URL: https://tldp.org/LDP/abs/html/index.html. — Дата обращения: 02.01.2025 г.
Python 3 documentation. — URL: https://docs.python.org/3/. — Дата обращения: 02.01.2025 г.
Лицензия
Текст статьи публикуется на условиях лицензии Creative Commons Attribution 4.0 International, достаточным условием атрибуции в плане авторства является указание ссылки на оригинальную статью с её названием.
Исходные коды, опубликованные в статье, доступны на условиях лицензии СС0 1.0 Universal, — примеры можно свободно использовать в своём коде без указания авторства.
Комментарии (101)
13werwolf13
08.01.2025 21:38всегда топлю за то чтобы скриптовать рабочие задачи именно на bash. да, питон даёт больше возможностей, но скрипт написанный на bash мной 10 лет назад всё ещё в проде ни как не изменившись, а скрипт написанный мной на питоне 5 лет назад уже трижды переписывался, сначала из pypi пропала какая-то важная для скрипта зависимость, потом между версиями питона сломали что-то.. радует только то что это уже не мои проблемы.
Алсо, в прошлом году довелось поучавствовать в созвоне где стильномодномолодёжный девопс предлагал переписать все скрипты на nodejs.. сказать что у меня волосы зашевелились по всему телу ничего не сказать..
venanen
08.01.2025 21:38сначала из pypi пропала какая-то важная для скрипта зависимость
Сдается мне, что если скрипт на питоне требует зависимостей - на баше его реализовать будет невероятно сложно.
derpymarine
08.01.2025 21:38Не всегда это является правдой. Иногда народ делает зависимости на какие-то совершенно тривиальные вещи.
Например: https://www.npmjs.com/package/is-number
polearnik
08.01.2025 21:38https://www.npmjs.com/package/is-number?activeTab=code почитал я код - нетривиальные проверки. я бы навскидку только Number.isNan( ) проверял бы. После парочки трудноуловимых багов пришел бы к коду как в библиотеке.
Andrey_Solomatin
08.01.2025 21:38просто ставите зависимости, тольк не через pip а через системный пакетный мереджер.
Кажется тот-же jq не везде идёт из коробки.
RH215
08.01.2025 21:38А для стабильности скриптов нужно использовать не pypi, а системные пакеты. :)
Хотя, ИМХО, Python-скрипты обычно стабильнее. Ибо CLI API любят ломать гораздо чаще, чем API библиотечный.
Andrey_Solomatin
08.01.2025 21:38С баш аргументами есть особенности когда пишешь и тестируешь на маке, а прод на линуксе. GNU и BSD командны не полностью совместимы по аргументам.
rexer
08.01.2025 21:38Это действительно важный аргумент, однако в противовес можно сказать, что Python предоставляет бОльшую гибкость, за что расплачивается как раз тем, что надо окружение настраивать дополнительно (должны быть стандарты по конфигурации серверов, внутренние репозитории-зеркала, надо больше отслеживать уязвимости и реагировать на них) . И не всегда эта гибкость нужна и даже уместна. Плюс научить стильноможномолодежных девопсов проще писать на Python. Поэтому тут надо смотреть под задачу/команду и выбирать с умом.
NeoCode
08.01.2025 21:38У меня волосы шевелятся когда я вижу любой код на bash)) Как вообще на этом можно что-то писать? Хотя Питон я тоже не люблю, но он хотя-бы придерживается классического синтаксиса языков программирования.
Нет, вполне понятно что любые скрипты командной строки - это в каком-то смысле развитие самой идеи запуска программ в командной строке, а значит синтаксис путей, аргументов командной строки, пайплайнов и т.п. должен быть в скриптах без изменений. Это накладывает существенные ограничения на синтаксис скриптов - строки не в кавычках, пробелы вместо запятых и прочая муть, делающая bash очень сильно отличающимся от любого классического языка. Не знаю можно ли было пойти по другому пути в принципе... Я не пишу "скрипты" как таковые, но если мне нужна простая утилита командной строки - я пишут ее на Go.
vadimr
08.01.2025 21:38В эту тему в истории вляпывались уже много раз. IBM неизменно приходила к языку REXX, а тут, видимо, питоном дело кончится:
VM/CMS: exec -> exec2 -> rexx
PC-DOS: bat -> rexx
OS/2: cmd -> rexx
*nix: sh -> bash -> python?
SkywardFire
08.01.2025 21:38Мысленно плюсую к данной позиции её, позиции, сдержанность. Вы хотя бы признаёте, что не являетесь скриптером. Окей. Именно поэтому спорить не буду.
Только лишь замечу, что вы интуитивно правильно сказали, действительно, дело именно в том, что shell-подобные языки должны быть в первую очередь удобными интерактивными интерпретаторами, и уже во вторую очередь скриптовыми языками, для выполнения команд по сложной логике.
В действительности же на bash можно писать даже довольно большие скрипты, и работать это будет великолепно.
kuzzdra
08.01.2025 21:38и работать это будет великолепно.
Только не забывать экранировать пробелы, кавычки, символы экранирования. Иногда 2 раза ;)
fillsa
08.01.2025 21:38Это какой такой классический синтаксис у питона? по моему там синтаксис отсутствует как понятие вообще. Эти дурацкие пробелы, с которыми постоянно куча проблем. Но главный подвох в том, что вроде бы все построено на пробелах, но ифы ты двоеточиями закрывай, да и циклы тоже закрывай и много чего еще и встает вопрос, а зачем тогда было делать все на пробелах, если по итогу я выборочно буду завершение строк проставлять?
Andrey_Solomatin
08.01.2025 21:38Если писать в блокноте, то и в скобочках можно запутаться. Я и на питоне и на языках в стиле С пишу. И там и там нормально. А еще код в сиподобных языках часто форматируют оступами, что выглядит очень похоже на Питон.
Keirichs
08.01.2025 21:38Так если у вас скрипты не изменяются десятилетиями, то почему бы не собрать бинарник питоновского скрипта?
severgun
08.01.2025 21:38Скрипты на sh обычно используют утилиты которые есть практически во всех дистрибутивах из коробки.
В случае если мне надо написать что то на питоне начинается пляска зависимостями. Которые нужно дополнительно устанавливать всегда. А если это изолированная машина и никакой pip недоступен, то это вообще гроб гроб кладбище.
Если задача стоит написать на более человеческом современном языке, то проще на go собрать один бинарник и залить на удаленный сервер, чем поднимать всё окружение python.
aleaksah
08.01.2025 21:38Для таких случаев придумали pyinstaller, который среди прочего может превратить скрипт со всеми зависимостями в один монолитный файл
Andrey_Solomatin
08.01.2025 21:38У Питона сильная стандартная библиотека. Часто можно обойтись вообще без зависимостей.
JBFW
08.01.2025 21:38А еще если кто помнит лет так много назад половину системных скриптов писали на Perl.
Но видимо новые программисты не осилили и придумали Python, чтобы делать то же самое.При этом, "развитие" Perl (а точнее переделка и нарушение совместимости) закончилось давным-давно, поэтому скрипты на нем 10-20 летней давности, еще для тех, медленных компьютеров с небольшим объемом памяти, работают и сегодня, только быстрее.
В отличии от скриптов на python2, например wicd, которые еще попробуй заставить работать с python3...
В этом смысле лучше всего чистый sh, оно будет работать везде, ну еще более-менее bash.
Но и там ломают совместимость, заменяя стандартные UNIX-утилиты типа ifconfig на ip, создавая потом эмуляторы ifconfig на python...В общем, нескучно людям.
Newbilius
08.01.2025 21:38Звучит как "новое поколение не осилило рассчёты на бумажке и решили использовать компьютеры". На Perl плохочитаемые скрипты сделать было даже проще, чем на bash ;)
RH215
08.01.2025 21:38На Perl плохочитаемые скрипты сделать было даже проще, чем на bash ;)
Я насмотревшись на подобные произведения современного админского искусства начал ценить то, что Python за такое бьёт по рукам. :)
JBFW
08.01.2025 21:38А кто заставляет писать плохо читаемые скрипты?
Разве что стремление показать свои глубокие знания языка.
Но если простые скрипты "плохо читаемые" это и называется "ниасилил" )
Andrey_Solomatin
08.01.2025 21:38Я не знаю кто писать заставляет, но читать их мне приходится.
В питоне синтаксис намного беднее чем в баше. Если нуно сравнить два объекта в питоне у вас ==, is и__equals__
. А в баше много вариантов.
А кроме баша нужно еще и синтакис системных комманд.
ildus
08.01.2025 21:38Чистый sh может и будет работать везде, но ведь в нем будут использоваться другие программы для всего, например grep, cut. И которые уже начинают работать по разному в разных ОС, придется проверять GNU ли версия (солярис), cut работает как-то по другому в AIX итд. Про специфичные утилиты типа ipconfig я вообще молчу.
sena
08.01.2025 21:38Для этого есть posix стандарт (и на sed и на другие стандартные утилиты), благодаря которому даже древний sh скрипт будет работать везде.
Gorthauer87
08.01.2025 21:38Не знаю, как по мне, перл это не то что хочется осиливать, его философия "There’s more than one way to do it" привела к избыточной сложности на ровном месте и засилию любителей однострочников, а дальнейшее развитие языка несколько раз заходило в тупик.
Так что я прекрасно понимаю желание людей свалить с него на Пайтон.
ABATAPA
08.01.2025 21:38привела к избыточной сложности на ровном месте
И где там "избыточная сложность", и, главное, что заставляет Вас её использовать?
Perl — не только гибкий, но и стройный язык.
ABATAPA
08.01.2025 21:38При этом, "развитие" Perl (а точнее переделка и нарушение совместимости) закончилось давным-давно
Ну, развитие какое-то есть. Но я до сих пор на нём регулярно пишу скрипты автоматизации, разборки конфигов и т. д. Он вообще и был предназначен для подобного ("Practical Extraction and Report").
Ну и Bash. Без него никуда, конечно.
event1
08.01.2025 21:38заменяя стандартные UNIX-утилиты типа ifconfig на ip
Задели за живое. Во-первых ifconfig не имеет никакого отношения ни к оболочке, ни к стадартам. Это просто отдельная утилита. Во-вторых, текущая версия ifconfig для линукс датирована октябрём 2001-го (!!!!) года. Тот факт, что она не только компилируется но и как-то работает, иначе как божьим промыслом, объяснить невозможно. Особенно учитывая что сетевой стек переписали раза 4 за это время. В-третьих, ifconfig основан на всратых сетевых ioctl-ах. Которые живы до сих пор, только потому, что "don't break the userspace". To ли дело iproute2 (к которому относится утилита ip), просто переводящий из английского в netlink-пакеты, которые только и позволяют описать всё богатство и разнообразие линуксового сетевого стека. Так что отмирание ifconfig — есть вселенское благо.
saege5b
08.01.2025 21:38Баш можно править везде без проблем, а вот питон чуть посложней - без айди будет очень напряжно.
Да и версионность питона напрягает: 2 отдельно, 3 ещё на несколько поколений делится.
Пару раз щупал бесплатные впс-ки со старыми системами типа шестого цента, где в качестве системного гвоздями приколочен питон2, а сбоку для юзверя - 3.2. Так себе впечатления.
Eugene_Rymarev
08.01.2025 21:38Делать выводы по каким-то бесплатным ВПС'кам довольно странно.
Автор, кстати, забыл упомянуть, что Python ещё можно собрать в exe'шник под linux/windows и он будет работать даже без Python'а в системе.
Статья отличная. Сравнения очень хорошо сделаны. Я отдаю своё предпочтение Python всё же.
В самой статье несколько раз встречал опечатки, но это не страшно. Например, "
loggong.DEBUG
" - надеюсь, что автор перечитает и всё поправит.Andrey_Solomatin
08.01.2025 21:38А shell=True в статье вас не смущает? Это же возможность писать баш прямо внутри Питона, чтобы собрать минусы обоих систем.
brownfox
08.01.2025 21:38Питон отлично программируется и без IDE, в простом текстовом редакторе. Тут особой разницы с баш-скриптингом нет.
В моей практике баш-скрипты чаще используются для автоматизации сборки и деплоймента продуктов, а питоновские - для разнообразного сбора и анализа данных. Стратегически это себя оправдывает :)
Fr0sT-Brutal
08.01.2025 21:38Никаких проблем с правкой Питона в mcedit нет, разве что надо четко соблюдать отступы. А Питон2 помер уж лет десять назад, едва ли стоит его подтаскивать в качестве актуального довода. При этом 3-ка вполне себе совместима снизу вверх
Ninil
Странный выбор для сравнения... Помню тут некоторое время назад кто то Airflow c NiFi сравнивал - из той же оперы....
RH215
Почему? Выбор "уже писать скрипт на python или ещё bash справится?" стоит часто.
Ninil
В реальных задачах за свои почти 20 лет опыта никогда не встречал такой дилеммы.
Можете привести реальные примеры рабочих задач? Не когда вам надо "для себя" что-либо сделать локально?
Newbilius
Автоматизировали CI силами разработчиков, попробовали оба варианта. В итоге стало однозначно понятно, что Python разработчиками на .NET и Kotlin читается и пишется гораздо проще, чем Bash)
vvzvlad
Для реальных рабочих задач можно всегда выбирать питон.
Aldrog
Когда половина кода — это вызовы внешних программ, то баш гораздо легче читается, не вижу ни одной причины для такого клея выбирать пайтон.
khajiit
Мониторим Proxmox. Возникла проблема: сам прокс отсылает метрики нечасто, в них отсутствуют дополнительные маунтпойнты, теги и пулы.
Пилежка своих метрик заканчивается разбором конфигов, мониторингом конфигов на предмет изменений, попыткой изобрести join в консоли…
Ninil
Если я правильно вас понял, вы пытаетесь решить задачу каким-то быстрым наколеночным решением. Это не плохо. Часто так приходится делать. Но это не для ПРОДа.
khajiit
Ну, у нас есть некоторые сервисы типа oneshot, которые отсылают метрики curl'ом — просто и без затей.
С самим проксом сначала задача была примитивной: вытащить в метрики теги для упрощения строительства дашборд. Часто их обновлять не надо, так что можно просто сделать это на баше.
Но задача стала раздеваться по мере выполнения, и баш справляться с ней перестал. Точнее, он перестал справляться удобно.
Ninil
Я думаю тут ключевое - "удобно". ОЧень часто простые и "правильные" решения обычному обывателю(в т.ч. разработчику) как раз неудобны. Но надо понимать, что "неудобность" - понятие субъективное
khajiit
Если в команде код понимает только один человек, то надо менять или команду или код ) Второе — проще.