Написание скриптов на Python вместо Bash/SH
#1
Сразу скажу, что использование Python для написания простых скриптов - оверкилл. Потому что, в отличие от скриптовых языков (Bash/SH), это полноценный язык программирования. Тем не менее в этом есть свои плюсы. Я так и не осилил скриптовые языки, и каждая попытка воспользоваться оными оборачивалась болью.

Если Вы хорошо знаете скриптовые языки и у Вас есть огромный опыт - то этот пост не для Вас. А всем остальным - добро пожаловать.

Для понимания статьи у Вас уже должны быть базовые знания питона.

Много кода было взято отсюда, не забудьте посетить ссылку: https://github.com/ninjaaron/replacing-b...ith-python

Причины

Начну с того, что этот пост не является агитацией за тот или иной язык, а является, скажем так, наглядным сравнением. Выбор остаётся за читателем.

Заголовок немного лукавит. Питон не является равноценной заменой скриптовым языкам, т.к. они состоят в разных весовых категориях. Однако питон имеет замену большинству возможностей Bash/SH. Разумеется, ни о какой замене всего Bash/SH на Python (в т.ч. для установки по умолчанию в терминале) речи не идёт.

Для себя я выбрал питон по следующим причинам:
  • Работа со строками. Т.к. это полноценный язык программирования, то в нём присутствует полноценная работа со строками. Парсинг, форматирование, разделение и т.д. Никакого жонглирования с grep и awk.
  • Различные типы переменных. Строки являются строками, числа - числами. Это позволяет нормально сравнивать переменные и проводить с ними операции.
  • Возможность писать код полностью без функций. Т.е. прямо как Bash/SH.
  • Полноценные циклы и итераторы
  • Обработка данных средствами языка, без вызова внешних программ. Bash/SH являются всего лишь клеем для внешних приложений.
  • Наличие API у различных штук, что позволяет не парсить выхлоп программы, а взаимодействовать с ней по-человечески
  • Да и в целом приятнее писать, благодаря синтаксическому сахару

А ещё питон позволяет избежать подобных нюансов:
Код:
foo = 'test   test1  test2'
echo $foo
# test test1 test2
echo "$foo"
# test   test1   test2

Можно вообще на любом языке писать, на самом деле, хоть на сишке. Но питон - это современный BASIC, не требующий предварительной компиляции, и входящий в поставку во многих дистрибутивах Linux, так что почему бы и не воспользоваться им.


Примеры

Некоторые примеры будут притянуты за уши, т.к... эээ... когда я садился за написание статьи, я не задумывался о том, что у меня уже давно не возникало каких-то задач по теме. Так что задачи будут абстрактными, но показанный код может быть применим в реальных задачах.

Начнём с простого. Есть модуль shutil, который реализовывает многие низкоуровневые вещи. Ещё есть модуль os.
Код:
shutil.move(src, dest) //mv
shutil.copy2(src, dest) //cp
shutil.copytree(src, dest) //cp -r
shutil.rmtree(dir) //rm -r
shutil.chown(file, user, group) //chown
shutil.which(file) //which
os.remove(file) //rm
os.rename(old_name, new_name) //mv
os.mkdir(dir) //mkdir
os.makedirs(dir) //mkdir -p

Например, вам надо произвести некую каталогизацию. В данном случае это будут директории с годом в названии, а в них - директории с номером месяца.
Код:
import os

base_path = "."

for year in range(2000, 2011):
    for month in range(1, 13):
        os.makedirs(f"{base_path}/{year}/{month}", exist_ok=True)

Я не знаю, почему верхняя граница диапазона требует указать на единицу больше. Тем не менее всё работает. Аргумент exist_ok=True указывает не выдавать ошибку, если директория уже существует.

Скрипт можно сделать чуть гибче:
Код:
import os
import sys

if len(sys.argv) < 2:
    print("Укажите базовый путь")
    sys.exit(-1)

base_path = sys.argv[1]

for year in range(2000, 2011):
    for month in range(1, 13):
        os.makedirs(f"{base_path}/{year}/{month}")

Теперь можно указать директорию, в которой будут создаваться другие директории, аргументом для скрипта. Что-то вроде python yourscript.py /home/user/your_folder/.

На самом деле у питона в стандартной библиотеке есть мощный модуль для работы с аргументами командной строки, но в этот раз обойдёмся без него.

В Python есть средства для получения информации. Давайте напишем небольшой скрипт, отображающий информацию о системе (что-то вроде screenfetch):
Код:
import os
import platform
import psutil


def get_uptime():
    with open("/proc/uptime", "r") as f:
        uptime = f.read().split(" ")[0].strip()

    uptime = int(float(uptime))

    return uptime

# Переменные

fetch_user = os.environ["USER"]
fetch_host = os.environ["HOSTNAME"]
fetch_de = os.environ["DESKTOP_SESSION"]
fetch_shell = os.environ["SHELL"]
fetch_home = os.path.expanduser("~")
fetch_os = platform.system();
fetch_kernel = platform.uname()
fetch_cpu = platform.processor()
fetch_dist = platform.freedesktop_os_release()
fetch_ram = psutil.virtual_memory()
fetch_swap = psutil.swap_memory()


print(f"{fetch_user}@{fetch_host}")
print(f"Shell: {fetch_shell}")
print(f"Home: {fetch_home}")
print(f"OS: {fetch_os}")
print(f"Distro: {fetch_dist['PRETTY_NAME']}")
print(f"Kernel: {fetch_kernel.release}")
print(f"Desktop: {fetch_de}")
print(f"CPU: {fetch_cpu}")
print(f"Memory (GB): {fetch_ram[0] // 1000000000} (total), {fetch_ram[3] // 1000000000} (used)")
print(f"Swap (GB): {fetch_swap[0] // 1000000000} (total), {fetch_swap[3] // 1000000000} (used)")
print(f"Uptime: {get_uptime() // 3600} hours, {(get_uptime() % 3600) // 60} minutes")
Пример выхлопа команды:
Код:
user@desktop
Shell: /usr/bin/zsh
Home: /home/user
OS: Linux
Distro: Fedora Linux 36 (KDE Plasma)
Kernel: 6.0.8-xm1.0.fc36.x86_64
Desktop: plasma
CPU: x86_64
Memory (GB): 16 (total), 9 (used)
Swap (GB): 17 (total), 0.0 (used)
Uptime: 9 hours, 51 minutes

Все модули отработали, кроме отображения модели процессора. Не знаю, только ли у меня такой косяк. Но раз модуль не отработал, то можно пропарсить /proc/cpuinfo, благо что в питоне это делается очень легко:
Код:
def get_cpu():
    with open("/proc/cpuinfo", "r") as f:
        for line in f:
            if "model name" in line:
                return(line.split(": ")[1].strip())


Здесь мы проверили строки на предмет содержания "model name" и разделили найденную строку по двоеточию с пробелом. В данном случае процессор у меня один, и я просто сделал возврат первого совпадения. Но что, если установлены два разных процессора?

Мы не будем учитывать количество ядер (т.к. я не знаю, как), только разные модели процессоров.

Поменяем функцию:
Код:
def get_cpu():
    cpu_list = []
    with open("/proc/cpuinfo", "r") as f:
        for line in f:
            if "model name" in line:
                cpu_model = line.split(": ")[1].strip()
                if cpu_model not in cpu_list:
                    cpu_list.append(cpu_model)
                   
    return cpu_list

И поменяем код вывода модели процессора:
Код:
if len(fetch_cpu) == 1:
    print(f"CPU: {fetch_cpu[0]}")
else:
    for cpu in fetch_cpu:
        print(f"CPU {fetch_cpu.index(cpu) + 1}: {cpu}")

Мы просто проверяем длину списка с процессорами, и в зависимости от неё выводим разный текст. Прибавление единицы можно и убрать, просто мне так больше нравится.

А вот пример реальной задачи, которая у меня возникла пару месяцев назад. Мне нужно было сжать образы игр .ISO для PSP. И вроде бы ничего особенного, всего лишь запустить бинарь с нужными аргументами. Но игр было довольно много.

Я уже писал выше, что Bash/SH - это клей для внешних приложений. И питон тоже может быть им!
Код:
import os
import subprocess
import sys

ISO_LIST = os.listdir(sys.argv[1])

FOLDER_NAME = sys.argv[2]

os.makedirs(FOLDER_NAME, exist_ok=True)

for iso in ISO_LIST:
    if iso.endswith(".iso"):
        print(f"Now compressing: {iso}")
        out_file = os.path.splitext(iso)[0] + '.cso'
        sp = subprocess.run(["./ciso", "9", iso, f"{FOLDER_NAME}/{out_file}"])

В данном скрипте в цикле запускается внешний бинарь, а у выходных файлов указывается расширение .CSO. За запуск внешних процессов отвечает модуль subprocess.
Код:
user@desktop ~/G/ciso> python ciso_all_isos.py . cso_files                                                 master?
Now compressing: From Russia with Love - 007.iso
Compressed ISO9660 converter Ver.1.02 by BOOSTER
Compress 'From Russia with Love - 007.iso' to 'cso_files/From Russia with Love - 007.cso'
Total File Size 732495872 bytes
block size      2048  bytes
index align     1
compress level  9
ciso compress completed , total size = 565886491 bytes , rate 77%
Now compressing: Family Guy.iso
Compressed ISO9660 converter Ver.1.02 by BOOSTER
Compress 'Family Guy.iso' to 'cso_files/Family Guy.cso'
Total File Size 1186463744 bytes
block size      2048  bytes
index align     1
compress level  9
ciso compress completed , total size = 1101765517 bytes , rate 92%

Вообще, пакетная обработка в питоне сделана довольно приятно. Вот ещё один пример. В нём я хочу залить несколько картинок на сервер https://0x0.st.
Код:
import os
import requests

FILE_LIST = os.listdir()

def upload_pic(filename):
    with open(filename, "rb") as fname:
        r = requests.post("https://0x0.st", files={"file": fname})
        return r.text.strip()

for f in FILE_LIST:
    if f.endswith(".jpg"):
        print(upload_pic(f))

Запускаем:
Код:
# python upload_pics.py
https://0x0.st/ohHP.jpg
https://0x0.st/ohHZ.jpg
https://0x0.st/ohHN.jpg
https://0x0.st/ohHq.jpg
https://0x0.st/ohHb.jpg

requests - неофициальный, но очень популярный модуль реализующий HTTP клиент.

Или наоборот, сделаем качалку:
Код:
import sys
import requests

with open(sys.argv[1], "r") as fname:
    FILE_LIST = fname.readlines()

    for line in FILE_LIST:
        line = line.strip()

        r = requests.get(line)

        outfile = line.split("/")[-1]
       
        with open(outfile, "wb") as ofname:
            ofname.write(r.content)

Закидываем ссылки в текстовик и запускаем:
Код:
python download_files.py files.txt

Пример парсинга файла. Подсчитаем количество уникальных IP адресов в логе веб-сервера Nginx.
Код:
import sys

LOG_FILE = sys.argv[1]
UNIQUE_IPS = []
ALL_IPS = 0

logfile = open(LOG_FILE, "r")

for line in logfile:
    ip = line.split(" ")[0]
    ALL_IPS += 1
    if ip not in UNIQUE_IPS:
        UNIQUE_IPS.append(ip)

logfile.close()

print(f"Unique IPs: {len(UNIQUE_IPS)}, all IPs: {ALL_IPS}")

Пример работы:
Код:
# python unique_ips.py access.log
Unique IPs: 215, all IPs: 4373

Можно было бы воспользоваться регуляркой, или добавить дополнительные проверки... но сам формат лог-файла простой. Хотя... А вдруг нам подсунули не лог-файл?
Код:
import sys
import re

LOG_FILE = sys.argv[1]
UNIQUE_IPS = []
ALL_IPS = 0
IP_REGEX = r"(?:[0-9]{1,3}\.){3}[0-9]{1,3}"

logfile = open(LOG_FILE, "r")

for line in logfile:
    ip = re.findall(IP_REGEX, line)[0]
    ALL_IPS += 1
    if ip not in UNIQUE_IPS:
        UNIQUE_IPS.append(ip)

logfile.close()

print(f"Unique IPs: {len(UNIQUE_IPS)}, all IPs: {ALL_IPS}")


Заключение

Python вполне может быть клеем для внешних программ. Благодаря средствам языка и своей довольно богатой библиотеке, можно писать скрипты, зачастую не прибегая к вызову внешних программ, полагаясь на обработку данных средствами самого языка.
It's time to kick gum and chew ass. And i'm all out of ass.
#2
Периодически использую его для каких-то более сложных, чем простой скрипт, задач. Но ИМХО есть один косяк — он обязательно должен быть в системе, а это не всегда так. Или не так версия, например, 2 вместо 3 и так далее.
#3
(17.01.2023 10:Jan)Zhbert Написал: есть один косяк — он обязательно должен быть в системе
Ты ж гентушник, portage написан на Python, его не может не быть! ☺

Ну а если серьёзно, то крайне сложно найти десктопный Linux без Python… по крайней мере без Python 3.

(17.01.2023 10:Jan)Zhbert Написал: Или не так версия, например, 2 вместо 3 и так далее.
Это Python 2 сейчас встретить крайне сложно найти. Разве что на какой-нибудь старой RHEL/CentOS на каком-нибудь редко обслуживаемом сервере.

---

(17.01.2023 10:Jan)Zhbert Написал: Периодически использую его для каких-то более сложных, чем простой скрипт, задач.
Против Python ничего не имею, и даже периодически на нём пишу, но всё чаще на flua (огрызок Lua 5.4 в базовой системе FreeBSD), просто потому что ставить Python не всегда целесообразно.

Я вообще не притязателен к синтаксису (но многословность жавы/шарпа для меня всё ещё перебор ☺), потому язык выбирается тот, на котором удобнее всего решать задачу. Под удобством для разных задач может подразумеваться эффективность/скорость, потребление ресурсов или даже скорость введения кода в работу (да-да, то самое «хуяк-хуяк и в продакшн» ☺).
Правила форума
[Новичкам] Как правильно задавать вопросы, чтобы Вам помогли

«Буду бить аккуратно, но сильно!» © Лёлик, х/ф «Бриллиантовая рука»
#4
(19.01.2023 03:Jan)mord0d Написал: Ты ж гентушник, portage написан на Python, его не может не быть! ☺

Дык давно уж нет Smile

(19.01.2023 03:Jan)mord0d Написал: Ну а если серьёзно, то крайне сложно найти десктопный Linux без Python… по крайней мере без Python 3.

А вот я в каком-то из не-мейнстримовых видел, вроде. Он просил поставить питон, когда я хотел скрипт запустить.



(19.01.2023 03:Jan)mord0d Написал: Я вообще не притязателен к синтаксису (но многословность жавы/шарпа для меня всё ещё перебор ☺), потому язык выбирается тот, на котором удобнее всего решать задачу. Под удобством для разных задач может подразумеваться эффективность/скорость, потребление ресурсов или даже скорость введения кода в работу (да-да, то самое «хуяк-хуяк и в продакшн» ☺).

Ну у меня тоже таких проблем вроде бы нет. Единственное, что при выборе из одинковых по сути языков я выберу скорее тот, которые мне больше по душе. Но это касается каких-то глобальных проектов, а не быстрых задачек.

Единственный момент, что мне всегда больше импонировали компилируемые языки, потому что ты в итоге получаешь бинарник, который можно запустить в любой момент на любой машине без плясок у установками джава-машин или интерпретаторов.



Плюс если ты захочешь передать свое поделие другим, то наличие требований к каким-то сторонним штукам все усложняет. Особенно, если отдаешь хомяку.

Например, бинарник или даже бандл с либами Qt виндовоз запустить сразу, а вот всякие пистоны и прочие джавы для его уже будут чем-то нетривиальным.
#5
(19.01.2023 07:Jan)Zhbert Написал: Дык давно уж нет [Изображение: smile.png]
Бывших гентушников не бывает! Это я тебе как бывший гентушник говорю! 🙃

(19.01.2023 07:Jan)Zhbert Написал: А вот я в каком-то из не-мейнстримовых видел, вроде. Он просил поставить питон, когда я хотел скрипт запустить.
Ну не знаю, я такого около десяти лет не встречал…

(19.01.2023 07:Jan)Zhbert Написал: Единственный момент, что мне всегда больше импонировали компилируемые языки, потому что ты в итоге получаешь бинарник, который можно запустить в любой момент на любой машине без плясок у установками джава-машин или интерпретаторов.
А когда у тебя зоопарк архитектур? А когда у тебя два MIPS, и один из них без floating point? А когда у тебя зоопарк ARM от 5 до распоследней?
Зависимости в репозитории наверняка найдутся, а компилять жирные бандлы на десяток архитектур это как минимум долго.
Правила форума
[Новичкам] Как правильно задавать вопросы, чтобы Вам помогли

«Буду бить аккуратно, но сильно!» © Лёлик, х/ф «Бриллиантовая рука»
#6
(19.01.2023 21:Jan)mord0d Написал: Бывших гентушников не бывает! Это я тебе как бывший гентушник говорю! 🙃

Ну да. У меня иногда руки чешутся... Но я держусь Smile

(19.01.2023 21:Jan)mord0d Написал: А когда у тебя зоопарк архитектур? А когда у тебя два MIPS, и один из них без floating point? А когда у тебя зоопарк ARM от 5 до распоследней?
Зависимости в репозитории наверняка найдутся, а компилять жирные бандлы на десяток архитектур это как минимум долго.

Возвращаемся к тому, что все зависит от целей и задач.
#7
(20.01.2023 07:Jan)Zhbert Написал: Ну да. У меня иногда руки чешутся... Но я держусь [Изображение: smile.png]
Ой, как будто в macOS нельзя компилять. Вопрос в другом — что компилять.

(20.01.2023 07:Jan)Zhbert Написал: Возвращаемся к тому, что все зависит от целей и задач.
Это всегда первостепенный вопрос.
Правила форума
[Новичкам] Как правильно задавать вопросы, чтобы Вам помогли

«Буду бить аккуратно, но сильно!» © Лёлик, х/ф «Бриллиантовая рука»

Перейти к форуму:

Пользователи, просматривающие эту тему: 1 Гость(ей)