• Письма Notion

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

    Итак, я написал текст в Ноушене и попросил руководство обсудить. На следующий день открываю почту на телефоне и вижу штук 20 уведомлений от Ноушена: пошли комментарии. Тыкаю, чтобы прочитать. А письма, сюрприз, выглядят так:

    Не знаю какой это кегль — второй или третий — но прочесть эти письма физически невозможно. Нужно зрение орла или увеличительное стекло. Словом, с телефона я ничего не прочел и смог это сделать только когда сел за комп с 4к-монитором.

    И вот опять, смотрите: сапожник без сапог. Как мы помним, Дропбокс, программа для работы с файлами, не умеет показывать файлы. А Ноушен, программа для текста, не может показать текст. Разработчики, вы вообще своим Ноушеном пользуетесь? Если бы я там работал, то в первый же день открыл бы тикет и долбил им каждый спринт — ваши сраные письма не читаются. Поправьте стили. Высылайте plain text вместо HTML, он мне нахрен не сдался. Просто чтобы можно было прочесть текст.

    Директор Ноушена без конца гонит какую-то графоманию, которую постят на Хакер-ньюз и Хабре. Подобно Грефу внедряет искусственный интеллект и машинное обучение (которые дают пустую строку). И при этом никто сделает нормальный шрифт в письмах. Просто стыд.

  • Эта удивительная Clojure: что на ней разрабатывают, чем она отличается от других языков и подходит ли для входа в программирование

    Эта статья была написана для одного издания, но по ряду причин ее не опубликовали. Размещаю здесь, чтобы материал не пропал. В подготовке статьи участвовали:


    Мы расспросили разработчиков на Clojure из сообщества clojure_ru. Выясняли, как применяют язык, что на нём пишут, легко ли на нём программировать.

    Что программируют на Clojure

    Павел: сфера применения Clojure в техническом плане — в основном веб и серверные приложения. На успешно работающий Clojure-код можно посмотреть, например, в продуктах Metabase и Penpot, их исходный код открыт.

    Но постепенно язык проникает и в другие области. ClojureScript работает в браузерах и других средах для JavaScript, с помощью проекта Esprit его уже запускают на микроконтроллерах, а сейчас развивают ClojureDart, чтобы захватывать мир Flutter. Конечно, не все эксперименты в итоге «взлетят», но такое разнообразие работающих проектов показывает, что применимость языка ограничена скорее настроениями разработчиков, чем самим языком.

    Если говорить о предметных областях, то в вакансиях и проектах с Clojure, о которых слышу я, эмпирически кажется, что финтеха больше, чем прочих. Даже компания, поддерживающая Clojure, Cognitect, принадлежит банку Nubank. Но кроме финтеха областей тоже хватает.

    Иван: сфера применения Clojure широка, она решает те же задачи, что Java, Python и другие языки. На ней пишут сетевые сервисы, бэкенд веб- и мобильных приложений. Clojure подходит для обработки данных из разных источников — баз данных, очередей, HTTP API — и часто служит их оркестратором.

    Существует ClojureScript — компилятор кода на Clojure в JavaScript. С его помощью создают браузерный фронтенд и мобильные приложения на базе React Native.

    Код на Clojure можно скомпилировать при помощи GraalVM и native image, получив бинарный файл. С этим подходом пишут утилиты командной строки, интерпретаторы, AWS Lambda и многое другое.

    Read more →

  • Сбер

    Постоянно пользуюсь Сбером, чтобы оплатить что-нибудь по QR-коду. Каждый раз удивляюсь анимации вверху экрана. За каким-то хреном кнопка с QR не зафиксирована, а выезжает справа. Как в рекламных полях, где анимацию делают ради анимации: что-то выехало, покачалось, мигнуло. Типа, управление вниманием.

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

    Если функция кнопки известна заранее, а также ее текст и оформление, никакой анимации не нужно. Может, балбесу-дизайнеру из Сбера нравится: у него эмулятор на мощной тачке, запущено одно приложение, все плавно. А потребитель видит слайдшоу и промахивается кнопкой.

    Дорогие дизайнеры! Засуньте уберите ваши анимации подальше. Они не нужны, вы делаете лишнюю и вредную работу.

  • The Mask library for Clojure

    (This is a copy of the readme file from the repository.)

    Mask is a small library to prevent secrets from being logged, printed or leaked in any similar way. Ships tags for Clojure, EDN and Aero.

    Why? Because I’ve been in such a situation three times, namely:

    • We don’t mask the secrets.
    • Someone logs the entire config.
    • Secrets have leaked!
    • Rotate all the keys, tokens, etc.
    • Change the team and face the same.

    This library is an attempt to break this vicious circle.

    Installation

    Leiningen/Boot:

    [com.github.igrishaev/mask "0.1.0"]
    

    Clojure CLI/deps.edn:

    com.github.igrishaev/mask {:mvn/version "0.1.0"}
    

    Usage

    The mask.core namespace provides mask and unmask functions. Pass a value to mask to make it safe for logging or printing in REPL:

    (in-ns 'mask.core)
    #namespace[mask.core]
    
    (def -m (mask "Secret123"))
    
    -m
    << masked >>
    
    (str "The password is " -m)
    "The password is << masked >>"
    

    Masking is idempotent meaning that you can mask the same value multiple times but the result will be one-level masked value:

    (-> -m mask mask mask)
    << masked >>
    

    To release a value from a mask, unmask it:

    (unmask -m)
    "Secret123"
    

    Unmasking is idempotent a well:

    (-> -m unmask unmask unmask)
    "Secret123"
    

    Note: the library treats nil as an error value that cannot be masked. You’ll get an exception:

    (mask nil)
    Execution error (IllegalArgumentException) at ... (core.clj:34).
    Cannot mask a nil value
    

    Masking an empty value signals you’re doing something wrong. Most likely you’ve missed a corresponding key or an environment variable. Thus, the further work makes no sense.

    Spec

    The mask.spec module provides the ::mask spec that checks if a value is really masked. An example from the tests:

    (let [config
          {:username "Ivan"
           :password #mask "secret"}]
    
      (is (s/valid? ::config config)))
    
    ;; true
    

    Clojure tag

    The built-in #mask tag wraps any value with a mask:

    => {:token #mask "abc123" :password "SecretABC"}
    
    {:token << masked >>, :password "SecretABC"}
    

    EDN tag

    There is a reader-edn function that acts like an EDN reader for the same tag:

    (let [source (-> "{:foo #mask 42}")]
      (edn/read-string {:readers {'mask reader-edn}}
                       source))
    
    ;; {:foo << masked >>}
    

    Aero tag

    To extend Aero with the #mask tag, import the mask.aero namespace:

    (require 'mask.aero)
    

    Then read a config with the tag:

    ;; config.edn
    {:foo #mask #env "SOME_PASSWORD"}
    
    ;; code
    (aero/read-config (io/resource "config.edn"))
    
    ;; {:foo << masked >>}
    

    The Aero dependency is not included. You’ve got to provide it by your own.

    Ivan Grishaev, 2023

  • Эй

    Почему-то большие компании не могут нормально составить письмо на русском. Будут ошибки или нелепые обороты. Сегодня получил письмо от Дискорда, где ко мне обращаются на “эй”:

    Сам ты эй! Понятно, это дурацкая калька с английского hey. Почему не нашли русского чувака, чтобы показать ему перевод перед отправкой? В больших фирмах всегда найдется русский, украинец, поляк или венгр. Даже если он кодер, пусть посмотрит. Он скажет, что обращение на эй в русском не только неестественно, но даже грубо.

    Жаль, не сохранил скриншоты Гугла и Godaddy. Иной раз такую дичь присылают, что хватаешься за волосы. Сейчас реже, но все равно.

  • Silent Hill 2

    Появился трейлер переиздания Silent Hill 2 — одной из главных игр моего детства. Наверняка вы его смотрели, но вот на всякий случай:

    Графика выше всяких похвал, сказать нечего. Однако у меня вопрос — что не так с этим трейлером? Не проматывая вниз, постарайтесь подумать.

    Read more →

  • Праздники

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

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

    Вот и думаешь: стоило ли переставлять кучу дел из-за одного праздника? Кому вообще всрались эти 23 Февраля, 8 Марта, Дни единства и все такое? Кто эти люди, что их празднуют? И как?

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

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

  • The DynamoDB library for Clojure

    (This is a copy of the readme file from the repository.)

    This library is a driver for DynamoDB written in pure Clojure. No AWS SDK, lightweight dependencies, GraalVM-friendly.

    Benefits

    • Free from AWS SDK. Everything is implemented with pure JSON + HTTP.
    • Quite narrow dependencies: just HTTP Kit and Cheshire.
    • Compatible with Native Image! Thus, easy to use as a binary file in AWS Lambda.
    • Clojure-friendly: supports fully qualified keyword attributes and handles properly them in SQL expressions.
    • Both encoding & decoding are extendable with protocols & multimethods.
    • Raw API access for special cases.
    • Specs for better input validation.
    • Compatible with Yandex DB.

    Installation

    Leiningen/Boot:

    [com.github.igrishaev/dynamodb "0.1.2"]
    

    Clojure CLI/deps.edn:

    com.github.igrishaev/dynamodb {:mvn/version "0.1.2"}
    

    Documentation

    At cljdoc.org (automatic build).

    API Implemented

    At the moment, only the most important API targets are implemented. The rest of them is a matter of time and copy-paste. Let me know if you need something missing in the table below.

    Target Done? Comment
    BatchExecuteStatement    
    BatchGetItem +  
    BatchWriteItem    
    CreateBackup +  
    CreateGlobalTable    
    CreateTable +  
    DeleteBackup    
    DeleteItem +  
    DeleteTable +  
    DescribeBackup +  
    DescribeContinuousBackups    
    DescribeContributorInsights    
    DescribeEndpoints    
    DescribeExport    
    DescribeGlobalTable    
    DescribeGlobalTableSettings    
    DescribeImport    
    DescribeKinesisStreamingDestination    
    DescribeLimits    
    DescribeTable +  
    DescribeTableReplicaAutoScaling    
    DescribeTimeToLive    
    DisableKinesisStreamingDestination    
    EnableKinesisStreamingDestination    
    ExecuteStatement    
    ExecuteTransaction    
    ExportTableToPointInTime    
    GetItem +  
    ImportTable    
    ListBackups    
    ListContributorInsights    
    ListExports    
    ListGlobalTables    
    ListImports    
    ListTables +  
    ListTagsOfResource    
    PutItem +  
    Query +  
    RestoreTableFromBackup    
    RestoreTableToPointInTime    
    Scan +  
    TagResource +  
    TransactGetItems    
    TransactWriteItems    
    UntagResource    
    UpdateContinuousBackups    
    UpdateContributorInsights    
    UpdateGlobalTable    
    UpdateGlobalTableSettings    
    UpdateItem +  
    UpdateTable    
    UpdateTableReplicaAutoScaling    
    UpdateTimeToLive    

    Who Uses It

    DynamoDB is a part of Teleward — a Telegram captcha bot. The bot is hosted in Yandex Cloud as a binary file compiled with GraalVM. It uses the library to track the state in Yandex DB. In turn, Yandex DB is a cloud database that mimics DynamoDB and serves a subset of its HTTP API.

    Usage

    First, import the library:

    (require '[dynamodb.api :as api])
    (require '[dynamodb.constant :as const])
    

    The constant module is needed sometimes to refer to common DynamoDB values like "PAY_PER_REQUEST", "PROVISIONED" and so on.

    The Client

    Prepare a client object. The first four parameters are mandatory:

    (def CLIENT
      (api/make-client "aws-public-key"
                       "aws-secret-key"
                       "https://aws.dynamodb.endpoint.com/some/path"
                       "aws-region"
                       {...}))
    

    For Yandex DB, the region is something like “ru-central1”.

    Both public and secret AWS keys are masked with a special wrapper that prevents them from being logged or printed.

    The fifth parameter is a map of options to override:

    Parameter Default Description
    :throw? true Whether to throw a negative DynamoDB response.
    :version "20120810" DynamoDB API version.
    :http-opt (see below) A map of HTTP Kit default settings.

    The default HTTP settings are:

    {:user-agent "com.github.igrishaev/dynamodb"
     :keepalive (* 30 1000)
     :insecure? true
     :follow-redirects false}
    

    Create a Table

    To create a new table, pass its name, the schema map, and the primary key mapping:

    (api/create-table CLIENT
                      "SomeTable"
                      {:user/id :N
                       :user/name :S}
                      {:user/id const/key-type-hash
                       :user/name const/key-type-range}
                      {:tags {:foo "hello"}
                       :table-class const/table-class-standard
                       :billing-mode const/billing-mode-pay-per-request})
    

    List Tables

    Tables can be listed by pages. The default page size is 100. Once you’ve reached the limit, check out the LastEvaluatedTableName field. Pass it to the :start-table optional argument to propagate to the next page:

    (def resp1
      (api/list-tables CLIENT {:limit 10}))
    
    (def last-table
      (:LastEvaluatedTableName resp1))
    
    (def resp2
      (api/list-tables CLIENT
                       {:limit 10
                        :start-table last-table}))
    

    Put Item

    To upsert an item, pass a map that contains the primary attributes:

    (api/put-item CLIENT
                  "SomeTable"
                  {:user/id 1
                   :user/name "Ivan"
                   :user/foo 1}
                  {:return-values const/return-values-none})
    

    Pass :sql-condition to make the operation conditional. In the example above, the :user/foo attribute is 1. The second upsert operation checks if :user/foo is either 1, 2, or 3, which is true. Thus, it will fail:

    (api/put-item CLIENT
                  "SomeTable"
                  {:user/id 1
                   :user/name "Ivan"
                   :user/test 3}
                  {:sql-condition "#foo in (:one, :two, :three)"
                   :attr-names {"#foo" :user/foo}
                   :attr-values {":one" 1
                                 ":two" 2
                                 ":three" 3}
                   :return-values const/return-values-all-old})
    

    Get Item

    To get an item, provide its primary key:

    (api/get-item CLIENT
                  "SomeTable"
                  {:user/id 1
                   :user/name "Ivan"})
    
    {:Item #:user{:id 1
                  :name "Ivan"
                  :foo 1}}
    

    There is an option to get only the attributes you need or even sub-attributes for nested maps or lists:

    ;; put some complex values
    (api/put-item CLIENT
                  "SomeTable"
                  {:user/id 1
                   :user/name "Ivan"
                   :test/kek "123"
                   :test/foo 1
                   :abc nil
                   :foo "lol"
                   :bar {:baz [1 2 3]}})
    
    ;; pass a list of attributes/paths into the `:attrs-get` param
    (api/get-item CLIENT
                  "SomeTable"
                  {:user/id 1
                   :user/name "Ivan"}
                  {:attrs-get [:test/kek "bar.baz[1]" "abc" "foo"]})
    
    ;; the result:
    {:Item {:test/kek "123"
            :bar {:baz [2]}
            :abc nil
            :foo "lol"}}
    

    Update Item

    This operation is the most complex. In AWS SDK or Faraday, to update an item’s secondary attributes, one should manually build a SQL expression that involves string formatting, concatenation and similar boring stuff.

    SET username = :username, email = :email, ...
    

    The ADD, DELETE, and REMOVE expressions require manual work as well.

    The present library solves this problem for you. The update-item function accepts :add, :set, :delete, and :remove parameters, either maps or vectors.

    The :sql-condition argument accepts a plain SQL expression. Should it evaluates as falseness, the item won’t be affected and you’ll get a negative response.

    Set Attributes
    (api/update-item CLIENT
                     table
                     {:user/id 1
                      :user/name "Ivan"}
                     {:attr-names {"#counter" :test/counter}
                      :attr-values {":one" 1}
                      :set {"Foobar" 123
                            :user/email "test@test.com"
                            "#counter" (api/sql "#counter + :one")}})
    

    The example above covers three various options for the :set argument. Namely:

    1. The attribute is a plain string ("Foobar"), and the value is plain as well.
    2. The attribute is a complex keyword (:user/email) which cannot be placed in a SQL expression directly. Under the hood, the library produces an alias for it and injects it into ExpressionAttributeNames.
    3. The attribute is an alias, and the value is a raw expression. To distinguish an expression from a regular string (e.g. email), there is a wrapper api/sql. The alias #counter should be declared in the :attr-names map.
    Add Attributes

    The :add parameter accepts a map of an attribute or an alias to a value. Imagine you have the following item in the db:

    {:user/id 1
     :user/name "Ivan"
     :amount 3
     :test/colors #{"r" "g"}}
    

    To increase the amount and add a new color into the colors set, perfrom:

    (api/update-item CLIENT
                     table
                     {:user/id 1
                      :user/name "Ivan"}
                     {:add {"amount" 1
                            :test/colors #{"b"}}})
    

    Result:

    {:Item
     {:amount 4
      :user/id 1
      :test/colors #{"b" "r" "g"}
      :user/name "Ivan"}}
    
    Remove Attributes

    To remove an attribute, pass the :remove vector. Each item of that vector is either a keyword attribute, a raw string expression, or an alias.

    (api/update-item CLIENT
                     table
                     {:user/id 1
                      :user/name "Ivan"}
                     {:attr-names {"#kek" :test/kek}
                      :remove ["#kek" "abc" :test/lol]})
    

    To remove an item from a list, pass a string like this:

    ;; item in the databalse
    {:tags ["foo" "bar" "baz"]}
    
    {:remove ["tags[1]"]}
    

    Use an alias when the attribute is a keyword with a namespace:

    (api/update-item CLIENT
                     table
                     {:user/id 1
                      :user/name "Ivan"}
                     {:attr-names {"#tags" :user/tags}
                      :remove ["#tags[1]"]})
    
    Delete Attributes

    In DynamoDB, the DELETE clause is used to remove items from sets. The update-item function accepts the :delete argument which is a map. The key is either a keyword or a string alias. The value is always a set:

    The item:

    {:user/id 1
     :user/name "Ivan"
     :user/colors #{"r" "g" "b"}}
    

    API call:

    (api/update-item CLIENT
                     table
                     {:user/id 1
                      :user/name "Ivan"}
                     {:delete {:user/colors #{"r" "b"}}})
    

    Result:

    {:Item #:user{:colors #{"g"} :id 1 :name "Ivan"}}
    

    Delete Item

    Simple deletion of an item:

    (api/delete-item CLIENT
                     table
                     {:user/id 1 :user/name "Ivan"})
    

    Conditional deletion: throws an exception when the expression fails.

    (api/put-item CLIENT
                  table
                  {:user/id 1
                   :user/name "Ivan"
                   :test/kek 99})
    
    (api/delete-item CLIENT
                     table
                     {:user/id 1 :user/name "Ivan"}
                     {:sql-condition "#kek in (:foo, :bar, :baz)"
                      :attr-names {"#kek" :test/kek}
                      :attr-values {":foo" 1
                                    ":bar" 2
                                    ":baz" 3}})
    

    In the example above, the "#kek in (:foo, :bar, :baz)" expression fails as the :test/kek attribute is of value 99. The item stays in the database, and you’ll get an exception with ex-info:

    {:error? true
     :status 400
     :path "com.amazonaws.dynamodb.v20120810"
     :exception "ConditionalCheckFailedException"
     :message "The conditional request failed"
     :payload
     {:TableName table
      :Key #:user{:id {:N "1"} :name {:S "Ivan"}}
      :ConditionExpression "#kek in (:foo, :bar, :baz)"
      :ExpressionAttributeNames {"#kek" :test/kek}
      :ExpressionAttributeValues
      {":foo" {:N "1"} ":bar" {:N "2"} ":baz" {:N "3"}}}
     :target "DeleteItem"}
    

    Query

    The Query target allows searching items that match a primary key partially or match some range. Imagine the primary key of a table is :user/id :HASH and :user/name :RANGE. Here is what you have in the database:

    {:user/id 1
     :user/name "Ivan"
     :test/foo 1}
    
    {:user/id 1
     :user/name "Juan"
     :test/foo 2}
    
    {:user/id 2
     :user/name "Huan"
     :test/foo 3}
    

    Now, to find the items whose :user/id is 1, execute:

    (api/query CLIENT
               table
               {:sql-key "#id = :one"
                :attr-names {"#id" :user/id}
                :attr-values {":one" 1}
                :limit 1})
    

    Result:

    {:Items [{:user/id 1
              :test/foo 1
              :user/name "Ivan"}]
     :Count 1
     :ScannedCount 1
     :LastEvaluatedKey #:user{:id 1
                              :name "Ivan"}}
    

    To propagate to the next page, fetch the LastEvaluatedKey field from the result and pass it into the :start-key Query parameter.

    Scan

    The Scan API goes through the whole table collecting the items that match an expression. This is not optimal yet required sometimes.

    (api/scan CLIENT
              table
              {:sql-filter "#foo = :two"
               :attrs-get [:test/foo "#name"]
               :attr-names {"#foo" :test/foo
                            "#name" :user/name}
               :attr-values {":two" 2}
               :limit 2})
    

    Result:

    {:Items [{:test/foo 2
              :user/name "Ivan"}]
     :Count 1
     :ScannedCount 2
     :LastEvaluatedKey #:user{:id 1
                              :name "Ivan"}}
    

    Both LastEvaluatedKey and :start-key parameters work as described above.

    Other API

    See the tests, specs, and dynamodb.api module for more information.

    Raw API access

    The api-call function allows you to interact with DynamoDB on a low level. It accepts the client, the target name, and a raw payload you’d like to send to DB. The payload gets sent as-is with no kind of processing or interference.

    (api/api-call CLIENT
                 "NotImplementedTarget"
                 {:ParamFoo ... :ParamBar ...})
    

    Specs

    The library provides a number of specs for the API. Find them in the dynamodb.spec module. It’s not imported by default to prevent the binary file from growing when compiled with GraalVM. That’s a known issue when introducing clojure.spec adds +20 Mbs to the file.

    Still, those specs are useful for testing and documentation. Import the specs, then instrument the functions by calling the instrument function:

    (require 'dynamodb.spec)
    (require '[clojure.spec.test.alpha :as spec.test])
    
    (spec.test/instrument)
    

    Now if you pass something wrong into one of the library functions, you’ll get a spec exception.

    Tests

    The primary testing module called api_test.clj relies on a local DynamoDB instance running in Docker. To bootstrap it, execute the command:

    make docker-up
    

    It spawns amazon/dynamodb-local image on port 8000. Now connect to the REPL and run the API tests from your editor as usual.

    Ivan Grishaev, 2023

  • Удалил

    Как и многие, я храню всякий хлам в канале Saved Messages в Телеграме: ссылки, идеи, напоминалки. Иногда перебрасываю креды к сервисам, если лень это делать через гист или менеджер паролей.

    Только что случилась забавная вещь. Удаляя сообщение из Saved Messages, не заметил, как кликнул по каналу и удалил его. Нажимая на подтверждение, подумал, что диалог какой-то странный, но палец было не остановить. Вжух — и канала не стало со всеми сообщениями.

    Read more →

  • Баш-скрипты

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

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

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

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

    Замечу, что сказанное не относится к случаям, когда вызывают несколько утилит, соединяя их каналы, например:

    > ps aux | grep java
    

    Это совершенно нормально. Я имею в виду скрипты, где шелл выступает в роли настоящего языка программирования, то есть:

    • активно используются переменные;
    • циклы, ветвления, переходы;
    • объявление и вызов функций;
    • импорт других скриптов.

    Все вместе это становится таким адом, что лучше уволиться, чем ворошить осиное гнездо (или, в английской идиоме — тараканье).

    Кусок скрипта откуда-то из интернета:

    no_more_args=1 ;
      -*)
       if [ "x$no_more_args" = "x1" ] ;
    then
        dirs[$num_dirs]="$1";
        let "num_dirs++"
    else
     if [[ "x${1:1:1}" != "x-" && "x$1" =~ "x-.*a.*" ]] ;
     then
         no_dots=0;
     fi
        opts="$opts $1";
       fi
      *)
       dirs[$num_dirs]="$1";
       let "num_dirs++"
     esac
     shift
    

    Для меня он выглядит как китайская грамота. Что такое [ "x$no_more_args" = "x1" ]? Что такое [[ "x${1:1:1}" != "x-" && "x$1" =~ "x-.*a.*" ]]? Как это читать и поддерживать?

    Во времена Unix шелл-скрипты были прорывом, с этим никто не спорит. Однако время не стоит на месте, и созданы более удобные инструменты. Вот несколько примеров.

    Если с проектом нужно делать много мелких вещей, подойдет make. Напомню, make — это набор именованных действий. Кроме вызова по отдельности их можно комбинировать, например собрать бек и фронт за раз. В любом проекте я завожу Makefile и помещаю туда все, что придет в голову, например:

    • сборку проекта
    • прогон тестов и миграций
    • очистку временных файлов и папок
    • запуск и остановку Докера
    • много чего еще

    Пример большого Make-файла можно посмотреть в репозитории с книгой.

    Далее: если нужны действительно сложные скрипты, возьмите Питон. Даже на голом Питоне писать в разы легче, чем на баше. Для Питона созданы библиотеки с изящным API, чтобы вызывать процессы, двигать файлы и прочее.

    Да, Питон требует окружения. Однако если у вас сложные скрипты, он окупается. Установку Питона и его барахла поместите в Makefile.

    Для управления машинами лучше писать плейбуки на YAML. Кроме Ansible, полно других утилит для конфигураций.

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

    Еще одна интересная мысль — генерация шелла из другого языка. Подобно тому, как из TypeScript производят JavaScript, можно произвести шелл из условного Лиспа или Питона. Когда я работал в Exoscale, у нас была библиотека для генерации шелла на Кложе. Я ей не пользовался, но само наличие говорит о том, что в руководстве понимали риски ручного скриптописания.

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

    В общем, чем раньше вы замените шелл-скрипт на что-то профильное, тем лучше.

Страница 1 из 61