В этой небольшой статье речь пойдет о том, можно ли легко использовать Python для написания скриптов вместо Bash/Sh. Первый вопрос, который возникнет у читателя, пожалуй, а почему, собственно, не использовать Bash/Sh, которые специально были для этого созданы? Созданы они были достаточно давно и, на мой взгляд, имеют достаточно специфичный синтаксис, не сильно похожий на остальные языки, который достаточно сложно запомнить, если вы не администратор 50+ левела. Помните, ли вы навскидку как написать на нем простой if?

if [ $# -ne "$ARGCOUNT" ]
then
    echo "Usage: `basename $0` filename"
    exit $E_WRONGARGS
fi

Элементарно правда? Интуитивно понятный синтаксис. :)

Тем не менее в python эти конструкции намного проще. Каждый раз когда я пишу что то на баше, то непременно лезу в поисковик чтобы вспомнить как писать простой if, switch или что-то еще. Присвоение я уже запомнил. :) В Python все иначе. Я хоть и не пишу на нем круглые сутки, но никогда не приходилось лезть и смотреть как там сделать простой цикл, потому что синтаксис языка простой и интуитивный. Плюс ко всему он намного ближе к остальным мейнстримовым языкам типа java или c++, чем Bash/Sh.

Также в стандартной и прочих библиотеках Python есть намного более удобные библиотеки чем консольные утилиты. Скажем, вы хотите распарсить json, xml, yaml. Знаете какой я недавно видел код в баше чтобы сделать это? Правильно:

python -c "import json; json.loads..." :)

И это был не мой код. Это был код баше/питоно нейтрального человека.

То же самое с регексом, sed бесспорно удобная утилита, но как много людей помнит как правильно ее использовать? Ну кроме Lee E. McMahon, который ее создал. Да впринципе многие помнят, даже я помню как делать простые вещи. Но, на мой взгляд, в Python модуль re намного удобнее.

В этой небольшой статье я хотел бы представить вам диалект Python который называется shellpy и служит для того, чтобы насколько это возможно заменить Bash на python в скриптах.

Велкам под кат.

Введение


Shell python ничем не отличается от простого Python кроме одной детали. Выражения внутри grave accent символов ( ` ) в отличие от Python не является eval, а обозначает выполнение команды в шелле. Например

`ls -l`

выполнит ls -l как shell команду. Также возможно написать все это без ` в конце строки

`ls -l

и это тоже будет корректным синтаксисом.

Можно выполнять сразу несколько команд на разных строках

`
echo test > test.txt
cat test.txt
`

и команды, занимающие несколько строк

`echo This is   a very long   line

Выполнение каждого выражения в shellpy возвращается объект класса Result

result = `ls -l

Это можно быть либо Result либо InteractiveResult (Ссылки на гитхаб с документацией, можно и потом посмотреть :) ). Давайте начнем с простого результата. Из него можно легко получить код возврата выполненной команды

result = `ls -l
print result.returncode

И текст из stdout и stderr

result = `ls -l
result_text = result.stdout
result_error = result.stderr

Можно также пробежаться по всем строкам stdout выполненной команды в цикле

result = `ls -l
for line in result:
    print line.upper()

и так далее.

Для результата есть также еще очень много синтаксического сахара. Например, мы можем легко проверить, что код возврата выполняемой команды равен нулю

result = `ls -l
if result:
    print 'Return code for ls -l was 0'

Или же более простым способом получить текст из stdout

result = `ls -l
print result

Все вышеперечисленное — это обзор синтаксиса вкратце, чтобы просто понять основную идею и не грузить вас всеми-всеми деталями. Там есть еще много чего и для интерактивного взаимодействия с выполняемыми командами, для управления исполнением команд. Но это все детали, в которые можно окунуться в документации (на английском языке), если сама идея вам покажется интересной.

Это ж не валидный синтаксис Python получается, как все работает то?


Магия конечно, как еще :) Да, друзья мои, мне пришлось использовать препроцессинг, каюсь, но другого способа я не нашел. Я видел другие библиотеки, которые делают нечто подобное, не нарушая синтаксиса языка вроде

from sh import ifconfig
print(ifconfig("wlan0"))

Но меня такой синтаксис не устраивал, поскольку даже несмотря на сложности, хотелось получить best user experience ©, а для меня это значит насколько это возможно простое и близкое к его величеству Шеллу написание команд.

Знакомый с темой читатель спросит, чем IPython то тебя не устроил, там ж почти как у тебя только значок другой ставить надо, может ты просто велосипедист, которому лень заглянуть в поисковик? И правда он выглядит вот так:

lines = !ls -l

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

import myipythomodule

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

В shellpy код переиспользуется легко и импортируется точно так же как и в обычном python. Предположим у нас есть модуль common в котором мы храним очень полезный код. Заглянем в директорию с этим модулем

ls common/
common.spy  __init__.spy

Итак, что у нас тут есть, ну во первых init, но с расширением .spy. Это и является отличительной чертой spy модуля от обычного. Посмотрим также внутрь файла common.spy, что там интересного

def common_func():
    return `echo 5

Мы видим что тут объявлена функция, которая внутри себя использует shellpy синтаксис чтобы вернуть результат выполнения `echo 5. Как этот модуль используется в коде? А вот как

from common.common import common_func

print('Result of imported function is ' + str(common_func()))

Видите? Как в обычном Python, просто взяли и заимпортировали.

Как же все работает. Это работает с помощью PEP 0302 — New Import Hooks. Когда вы импортируете что-то в своем коде то вначале Python спрашивает у хука, нет ли тут чего-то твоего, хук просматривает PYTHONPATH на наличие файлов *.spy или модулей shellpython. Если ничего нет, то так и говорит: "Ничего нету, импортируй сам". Если же он находит что-то там, то хук занимается импортом самостоятельно. А именно, он делает препроцессинг файла в обычный python и складывает все это добро в temp директорию операционной системы. Записав новый Python файл или модуль он добавляет его в PYTHONPATH и за дело берется уже самый обыкновенный импорт.

Давайте же скорее посмотрим на какой-нибудь пример


Этот скрипт скачивает аватар юзера Python с Github и кладет его в temp директорию

    import json
    import os
    import tempfile

    # с помощью curl получает ответ от апи гитхаба
    answer = `curl https://api.github.com/users/python

    # синтаксический сахар чтобы сравнить результат выполнение с нулем
    if answer:
        answer_json = json.loads(answer.stdout)
        avatar_url = answer_json['avatar_url']

        destination = os.path.join(tempfile.gettempdir(), 'python.png')

        # в этот раз скачиваем саму картинку
        result = `curl {avatar_url} > {destination}
        if result:
            # если проблем не возникло, то показываем картинку 
            p`ls -l {destination}
        else:
            print('Failed to download avatar')

        print('Avatar downloaded')
    else:
        print('Failed to access github api')

Красота...

Установка


Shellpython можно установить двумя способами: pip install shellpy или склонировав репозиторий и выполнив setup.py install. После этого у вас появится утилита shellpy.

Запустим же что-нибудь


После установки можно потестировать shellpython на примерах, которые доступны прямо в репозитории.

shellpy example/curl.spy

shellpy example/git.spy

Также здесь есть allinone примеры, которые называются так, потому что тестируют все-все функции, которые есть в shellpy. Загляните туда, чтобы лучше узнать что же там еще такого есть, либо просто выполните

shellpy example/allinone/test.spy

Для третьего Python команда выглядит вот так

shellpy example/allinone/test3.spy

Совместимость


Это работает на Linux и должно работать на Mac для Python 2.x и 3.x. На виндовсе пока не работает, но проблем никаких для работы нет, так как все писалось с использованием кроссплатформенных библиотек и ничего платформоспецифичного в коде нет. Просто не дошли руки еще, чтобы потестировать на виндовсе. Мака у меня тоже нет, но вроде у друга работало :) Если у вас есть мак и у вас все нормально, скажите пожалуйста.

Если найдете проблемы — пишите в коммент, либо сюда Join the chat at https://gitter.im/lamerman/shellpy либо телеграфируйте как-нибудь :)

Документация (на английском)


Wiki

Можно ли законтрибьютить


Конечно :)

Оно мне ничего в продакшене не разломает?


Сейчас версия 0.4.0, это не стейбл и продакшн процессы пока лучше не завязывать на скрипт, подождав пока все отладится. Но в девелопменте, CI можно использовать вполне. Все это покрыто тестами и работает :) Build Status

P.s.


Пишите ваши отзывы об идее в целом и о реализации в частности, а также о проблемах, пожеланиях, всех рад услышать :) Заводите Issues еще в гитхабе, там их уже много :)

Комментарии (34)


  1. RPG
    21.02.2016 18:36
    +6

    Выглядит очень интересно, хотя я и привык к shutil и простой обертке над subprocess, но никогда не писал на питоне то, для чего шелл подошёл бы лучше. Из статьи непонятно, умеет ли библиотека делать конвейер? Ведь шелл-простыни зачастую намного сложнее, что-то вроде такого:

    while read a b c; do
      grep $(sed s/1/2/ <<< $a) ... && some_command || error_handler
    done <(find ... | grep ... | tr ... | sort -u)

    В "нормальных" языках как раз часто не хватает главной фичи юникс-вея — конвейеров и всех тех непонятных штук, которые знакомы администраторам 50-го уровня:).

    P. S. Интересно, кто-нибудь подсчитывал, сколько в приведённых трёх строчках шелла строк кода на Си (ну ладно, хотя бы на Питоне). Естественно, если делать по-честному, не прибегая к вызову system.


    1. lamerman
      21.02.2016 18:47
      +5

      Да, это возможно, ведь то, что внутри '' по сути исполняется /bin/sh, например

      result = `ls -l | grep myfile
      


      1. bolk
        21.02.2016 20:10

        Надо просто сделать специальные объекты-обёртки и переопределить у них магический метод, отвечающий за «|» и можно будет делать конвееры.


        1. lamerman
          21.02.2016 20:12
          +2

          Зачем, если и так работает? :)


          1. bolk
            21.02.2016 20:16

            Как «так»? Автор же предпроцессор написал, то есть это не Пайтон.


          1. mayorovp
            21.02.2016 20:16
            +1

            Чтобы можно было вставлять команды на питоне посреди конвейера


            1. lamerman
              21.02.2016 20:35
              +2

              mayorovp, насколько я знаю, поправь меня если ошибаюсь, конвейеры не типичны для python. Там где они повсеместно используются, в шелл скриптах, вот это поддерживается в библиотеке, как видно в комментарии выше.
              Если же нужны контейнеры прямо для python то видимо можно использовать какие-то библиотеки для python, которые делают конвейеризацию, но я такого никогда не видел и ни разу не использовал. То есть я не делал никогда ничего наподобие 'text' | re.find('e') | print. Ведь об этом речь я так понимаю.



              1. fshp
                22.02.2016 17:31
                -1

                Пайп легко определить через левоассициативную бинарную функцию. На псевдокоде: x | f = f x
                Единственное преимущество шелловских пайпов это то, что все компоненты конвейера работают параллельно.


                1. mayorovp
                  23.02.2016 17:11

                  Это не "единственное преимущество", это "ключевое отличие".


  1. hellman
    21.02.2016 18:46
    +7

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

    парсить как положено? например как в https://github.com/ikotler/pythonect

    `curl {avatar_url} > {destination}

    если avatar_url = "; rm -rf /", что будет? от баша например можно ожидать, что echo $var не выполнит лишних команд (хотя $var и может раскрыться в несколько аргументов), а в этом вашем shellpy всё плохо


    1. lamerman
      21.02.2016 21:10
      +1

      Я не видел pythonect, нужно посмотреть что он делает и как это реализовано внутри.

      Сейчас {avatar_url} будет вставлен как есть, спасибо за обнаружение потенциальной проблемы, я подумаю насколько это критично и что с этим можно сделать.


  1. MrFrizzy
    21.02.2016 19:00
    +5

    А еще есть xonsh, но я не знаю, как у него обстоят дела с предыдущим примером


    1. moigagoo
      21.02.2016 20:50
      +1

      Использую его в качестве основного шелла, очень доволен. У него вполне цельная модель разделения ролей между Питоном и Башем.


  1. random1st
    21.02.2016 19:43
    +3

    Python по дефолту есть в любом дистрибутиве Linux. Зачем нужен Ваш shellpy — понятия не имею. Есть прелестный модуль sh.


    1. lamerman
      21.02.2016 20:38
      +2

      Он хороший, но лично мне он не показался очень удобным, наверное, это дело вкуса :)


  1. random1st
    21.02.2016 19:56
    +2

    Моя практика показывает, что для работы в консоли удобно использовать команды шелла, однако для написания скриптов лучше использовать питон. Кроме того, вызов каким-либо образом команд и консольных утилит из питона затрудняет отладку и приводит к побочным эффектам. bash после того, как освоил fish, не использую вообще.


    1. lightman
      21.02.2016 20:39
      +2

      Кстати на странице упомянутого MrFrizzy xonsh'а есть табличка сравнения в т.ч. с fish
      http://xon.sh/#comparison

      xonsh выходит самый мощный


      1. random1st
        21.02.2016 20:44
        +1

        Внимательнейшим образом просмотрел.fish позиционируется именно как shell, и в этом качестве он превосходен, в то время как xonsh предполагается использовать для написания скриптов.


        1. moigagoo
          21.02.2016 20:51

          Я использую xonsh именно как шелл. Перешел с zsh и не оглядывался.


          1. frol
            22.02.2016 05:36
            +2

            Я очень долго жил на bash и только в прошлом году решил снова поэкспериментировать с этими новомодными окружениями. zsh решил пропустить, а начал с xonsh, так как казалось, что это может быть удобно. В общем, не ужился я с ним из-за странностей (автодополнение мне всё время мешало понять что же я пишу) и отсутствия достаточно базовых вещей вроде && и || в bash, ну и поддержка virtualenv кривая (и это в shell'e на Python для любителей писать проекты на Python). А вот fish — действительно оказался очень приятной штукой, тут и остановился (даже небольшой скринкаст записал).


            1. moigagoo
              22.02.2016 14:09

              Буквально неделю назад добавил в xonsh менеджер окружений :-) Есть, что улучшить, но в целом проблема решена.

              Проекту есть, куда расти, не спорю.


      1. Crandel
        22.02.2016 11:19
        +1

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


  1. stagnantice
    21.02.2016 20:15
    +2

    Не всегда все так ужасно в bash, можно использовать обычные операторы сравнения, вместо -ne и прочих.
    Самому понравилось использовать в питон argparse, для консольных команд. А его уже вызывать bash скриптом например.


  1. maydjin
    21.02.2016 21:45
    +1

    Наш ответ PS ?:)

    Мне нравится. Имхо, в первую очередь надо подумать в сторону реализации/реюза readline для такого шелла. Опять же имхо — если основной юзекейс скрипты, то шансов взлететь гораздо меньше, без удобного интерактивного режима в смысле.


  1. bodqhrohro
    22.02.2016 02:54
    +3

    вы хотите распарсить json, xml, yaml
    Есть jq/xmlstarlet/shyaml.
    то непременно лезу в поисковик
    Вместо того, чтобы в мануал глянуть, который уже на диске лежит. Обычно.
    синтаксис языка простой и интуитивный
    У шей он тоже простой и интуитивный, если допереть, что «ключевые слова» — это на самом деле имена команд, соответственно, они всегда стоят в начале и не более одного на команду. А так-то у пыхтона по сравнению с «мейнстримными языками» синтаксис не менее странный. И привязка синтаксиса к форматированию многих отпугивает. Иногда красивее однострочник написать, чем длинную узенькую «лестницу».


    1. lamerman
      22.02.2016 09:55

      Есть jq/xmlstarlet/shyaml

      Речь не о том, что что-то есть в принципе, а о удобстве использования.

      Вместо того, чтобы в мануал глянуть, который уже на диске лежит. Обычно.

      Зачем лезть в мануал для того чтобы посмотреть синтаксис одного оператора (пожалуйста, не спутайте это с "зачем вообще лезть в мануал", я этого не говорил :) )? Поисковик даст ответ мгновенно, а мануал надо сначала найти, потом в нем найти if.


      1. bodqhrohro
        22.02.2016 23:13

        а о удобстве использования
        Ну и чем впихивание в однострочник скриптов на другом языке удобнее рассчитанных на CLI утилит с компактным синтаксисом запросов?
        Поисковик даст ответ мгновенно
        Если соединение быстрое. И даже при этом надо пролистать пару-тройку сайтов.
        мануал надо сначала найти
        Прям так сложно man bash набрать, ага.
        потом в нем найти if.
        А вот с этим там проблема, да, дюжеть большой мануал, и коллизий на поиск много, даже под /^\s+if много подпадает. С другой стороны, если пролистать его разок весь, то можно запомнить, что команды в конце.


    1. Ayahuaska
      22.02.2016 22:53

      Справедливости ради, питонячьи однострочники тоже вполне бывают.

      PS>
      Автор хотел велосипед — автор его собрал (:


  1. l0rda
    22.02.2016 03:59
    +3

    Смею заверить, что на баше можно писать очень красивые и элегантые скрипты, просто нужно прочитать ман. А тот же sed, awk, cut и тп, запоминаются раз и навсегда, хотя те кто пользуются mc, про них часто даже и не знают. Сам пишу на питоне, но как по мне смешивать все в кучу — ужасно, перфекционист во мне категорически против.


    1. bodqhrohro
      22.02.2016 23:21
      +2

      awk — вообще отдельный язык с кучей фич. На нём целиком довольно крупные скрипты пишут. Ну и sed — не промах, помимо попсового s/foo/bar/ там ещё куча полезных возможностей, из самого простого — печать определённого диапазона строк (sed -n 17,19p). А ещё теоретически на нём можно реализовать любой нормальный алгоритм Маркова, но это уже из разряда извращений.


  1. KawaiDesu
    22.02.2016 04:05
    +10

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


    1. sebres
      22.02.2016 21:01
      -1

      А если он где вдруг и нужен, то для этого есть другие шелл-подобные языки, например тот же TCL (действительно "интуитивно понятный", а главное шелл-похожий)…

      Python я тоже люблю и уважаю, но никогда не взял бы его как "shell replacement"...

      Вот как приведенный пример будет на TCL, причем без всяких препроцессингов, т.е. OOTB:

      #!/usr/bin/tclsh
      package require json
      
      set destination ""
      if {[catch {
        # set answer [exec curl https://api.github.com/users/python 2>1&]
        set answer [exec curl -s https://api.github.com/users/python]
      
        set answer_json [::json::json2dict $answer]
        set avatar_url [dict get $answer_json avatar_url]
      
        set destination [file join [exec mktemp -d -t] python.png]
      
        exec curl -s $avatar_url > $destination
      
        puts "Avatar downloaded: [exec ls -l $destination]"
      
      } err opt]} {
        puts "Failed to download avatar: $err"
        if {$destination ne ""} {file delete -force [file dirname $destination]}
        puts $opt
      }


  1. eps
    25.02.2016 20:42
    +2

    Пробовал писать shell-скрипты на python вместо bash, и запомнил про это три вещи:

    • Скрипты стали красиво выглядеть
    • Вообще нету особой уличной магии с экранированием, пробелами и кавычками
    • Даже большой скрипт через пол-года понятен. Ясно, что он делает, и что хранится в каждой переменной.