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

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

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

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

Сейчас я вам это покажу. Будет интересно, но впереди много боли. Я предупредил.


Итак, у core-разработчиков есть туча способов создать проблему там, где её нет. Они и создают. Для простоты я разделил по категориям.

❯ Наказание за забывчивость

Принцип простой: добавьте какую-нибудь ненужную рутину и создайте наказание за то, что пользователь про эту рутину забудет.

Подстановка в bash

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

FOLDER="My Documents"
rm $FOLDER

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

Кстати, двойные и одинарные кавычки ведут себя по-разному, за что отдельное спасибо! Почему не сделать как в питоне, где есть явно r-string и f-string?

Макросы в C

В C макросы — это тоже тупая подстановка, поэтому не дай Бог вы забудете написать макрос без заключения в скобочки — результат, опять же, непредсказуем.

#define SQR(x) x * x
int b = SQR(1 + 2);

Поведение Bash

Моё любимое. Представьте, что вы готовите блюдо по инструкции, и там где-то в середине написано «положите всё в духовку». Вы так и делаете, и тут вдруг духовка взрывается и сносит половину вашего дома! Ваши действия? Конечно, продолжить готовить! Достаёте горящие угли вперемежку с ошмётками кухни, раскладываете по тарелкам и подаёте гостям. Ну, по крайней мере, авторы Bash так и делают, ведь даже если какая-то команда отработала с ошибкой, то это не повод останавливаться.

В итоге в каждом, мать его, скрипте нужно писать set -euo pipefail, иначе он просто непредсказуем. Если хоть раз забыть написать эту магическую команду, как в скрипте ниже...

#!/bin/bash
(dos-make-addr-conf | tee config.toml) && dosctl set template_vars config.toml

... то можно, к примеру, положить cloudflare.

Назови по-тупому

Это когда разрабы хотели подложить свинью, но торопились или фантазии не хватило, и они решили просто по-быстрому запутать. Ну и придумали тупое название.

В postgres по умолчанию создаётся 3 базы данных. Не default, frozen_template и template, а postgres, template0, template1. Э?

Let's encrypt сложит challenge-файлы в /.well-known/acme-challenge/. Почему well-known? Потому что хорошим разработчикам этот факт хорошо знаком, что тут непонятного? На самом деле пошло от well known URIs, но как же меня бесит это название! А точка в .well-known как бы означает, что это скрытая папка, хотя в URL такой концепции нет, и зачем вообще скрывать папку, которая, наоборот, должна быть максимально публичной?

Javascript придумал отображать объекты [object Object], и, честно говоря, я бы и сам лучше не придумал!

const user = { name: "Alex" };
console.log("User: " + user);

// User: [object Object]

В python-регулярках мы назовём поиск всех вхождений re.findall(), а поиск одного - нет, не re.find(), а re.search().

Да я уверен, тупых названий вы сами видели миллион и одну штуку.

Иногда я смотрю на какое-то название и представляю себе автора, который его придумывал:

❯ Обнуление предыдущих знаний и отсутствие логики

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

Bash

Прям как синтаксис в Bash:
- if должно закрываться fi. Значит, for должно закрываться rof, а while - elihw? А вот и нет, только if-fi. В остальных done.
- Переменная окружения и значение по умолчанию: ${ENV_VAR:-10}. Нет, значение по умолчанию не -10, а 10! Зачем там дефис? Я хз.
- && - это что-то вроде логического И. & — это запуск процесса в фоне. Можете сами придумать какие-нибудь интересные кейсы при случайной замене одного на другое, но, скорее всего, придумывать ничего не надо, и с вами такое уже случалось.
- Для выполнения математических операций в файле скрипта можно использовать конструкцию вида $((a+b)). Чем мы провинились? Где согрешили? Я не знаю.
- Много всякого можно найти на shellcheck wiki, но иногда лучше что-то и не знать, наверно.

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

Команды

useradd / adduser

Есть ещё useradd и addusergroupadd с addgroup), сколько я их не использовал, всё равно не запомнил, какая что делает.

useradd вообще классная, там есть короткие флаги -g и -G, -M и -m, -p и -P, которые непонятно зачем вообще есть (когда есть нормальные длинные) и, главное, непонятно, по какому принципу выбраны:

-g, --gid GROUP          name or ID of the primary group of the new account
-G, --groups GROUPS      list of supplementary groups of the new account
-m, --create-home        create the user's home directory
-M, --no-create-home     do not create the user's home directory
-p, --password PASSWORD  encrypted password of the new account
-P, --prefix PREFIX_DIR  prefix directory where are located the /etc/* files

Какого вообще чёрта --create-home и --no-create-home существуют одновременно, ведь если есть поведение по умолчанию, то нужен только противоположный флаг?

Unix-way. Даже вдвойне

Смотрите, как одна программа делает одну вещь, и делает её хорошо:

  • find -exec {} \; — найдёт файлы/папки и выполнит над ними указанную команду; за синтаксис с \; и использование -exec вместо --exec ставлю отдельный плюс! А ещё можно делать сразу три действия — искать, проверять на пустоту и удалять: find . -type d -empty -delete. Это уже не unix way, это unix highway!

  • tar -cvz не только создаёт tar, но и делает gzip сжатие

Синтаксис команд

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

Про find -exec я уже говорил. Рядом в котле варятся tar xvf и ps axf, которые позволяют писать вообще без дефисов, потому что так интереснее! Или вот тоже: dd if=input.img of=output.img bs=4M.

Можно ещё сделать какие-то постоянно используемые флаги выключенным по умолчанию, ха-ха. Я на уровне мышечной памяти пишу rsync -avzP, потому что так оно работает нормально, но какой там флаг за что отвечает — я не помню.

Обработка текста

Результат работы команд — не json, а текст. Но при этом мы автоматизируем всё командами и делаем pipe одних команд в другие. Поэтому в скриптах появляются grep, sed и awk, которые пытаются распарсить вывод для человека и подготовить его для следующей команды.

Это же тупость! Программа преобразует чистые данные в текст, чтобы потом извлечь из текста чистые данные и передать дальше, а там опять такая же чехарда. А знаете, кто за это платит? Мы, когда опять пишем какую-то нечитаемую белиберду на awk.

Логи

Я даже не знаю, где хранятся логи: то ли в /var/log/, то ли в journalctl, то ли в dmesg. Даже если я узнал, где нужные логи, то у каждой утилиты свой формат логов — если посмотреть dmesg, там какой-то ад.

Crontab

А ещё я постоянно гуглю crontab формат, потому что мой мозг просто его отвергает. Я когда вижу формат МИНУТЫ ЧАСЫ ДЕНЬ-НЕДЕЛИ, у меня начинается эпилепсия. Или там другой формат? Видите, я опять не могу вспомнить.

Python

В python, например, можно вызывать функцию с позиционными аргументами calculate(1, 2, Operation.SUM), а можно с именованными calculate(arg1=1, arg2=2, operation=Operation.SUM). Именованные аргументы всегда лучше, потому что они явные и не ломаются при рефакторинге. Мой друг как-то час дебажил, потому что перепутал порядок аргументов.

И казалось бы: разрешите только один позиционный аргумент, а если аргументов 2 или больше — заставляйте их указывать по имени. Но знаете, в питоне подумали и сделали, что можно запретить kwargs. Типа одни разработчики могут заставлять других разработчиков писать хрупкий код. Отличный план :D

Шутка для таких же стариков, как я
Шутка для таких же стариков, как я

IP

Ну ладно, я привык к 127.0.0.1, хотя какого хрена? Я привык к 192.168, хотя какого чёрта? Я привык к 172.17, хотя какого лешего?

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

CSS

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

display: flex;
justify-content: center; /* Горизонтальное центрирование */
align-items: center; /* Вертикальное центрирование */

Pro-версия: css сделали настолько тупо, что никто всё ещё не умеет центрировать элементы.

YAML

Пишете вы docker-compose.yml. И делаете переменную KEY пустой.

Это запишет в переменную окружения KEY пустое значение:

environment:
  KEY:
  OTHER_KEY: 123

Это передаст значение KEY с хоста внутрь контейнера:

environment:
  - KEY
  - OTHER_KEY=123

Во-первых, почему можно написать одно и то же разными способами? Во-вторых, почему работает по-разному?!

❯ Сокрытие сложности

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

Go

Вот у нас есть dns резолвер системный в linux, это ж здорово! Можно почитать статью на Хабре, потом её продолжение, потом третью часть, потом умереть от старости пока читал, но всё-таки понять, как оно всё работает. А потом взять Go, а он, оказывается, подумал-подумал и решил, что к чёрту этот Линупс ваш, он будет использовать свой собственный резолвер, с блекджеком и resolv.conf.

Python + Django

В python есть дескрипторы, чтобы доступ к атрибуту был непредсказуемым — может выполниться всё что угодно. В Django этим воспользовались, и там доступ к атрибуту может неявно сделать запрос к БД. Типа написали вы так:

for source in sources[:100]:
    print(source.account)

А это 100 скрытых запросов к БД! Джанго не скажет, что вы забыли сделать JOIN, он просто будет долбать БД. То есть фреймворк по умолчанию спроектирован так, чтобы появлялась проблема N+1, а язык спроектирован так, чтобы это можно было сделать. Комбо!

❯ Просто заложи ловушку

Bash

NAME=value
NAME= value
NAME = value

Первое присвоит переменной NAME значение value.
Второе присвоит NAME пустое значение и вызывает value.
Третье вызовет команду NAME с аргументами = и value.

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

Ещё мне КРАЙНЕ НРАВИТСЯ, что bash кладёт огромный болт на неопределённые переменные и считает их пустой строкой. Это гениальное решение позволило успешно удалить данные с сетевого хранилища целого универсисета в Киото (открывайте ссылку с осторожностью, там реально жёсткое технопорно).

Динамическая и слабая типизация

Если вы выбираете динамическую типизацию для языка, то вы сразу выбираете ошибки в рантайме — ведь обязательно где-то проскочит не тот тип и положит, например, Cloudflare. Мне не нравится, что пишут «python — динамически типизированный язык». Ведь должно звучать так: «python — язык с ошибками в рантайме и сложными тулзами для интроспекции и проверки типов». Звучит уже не так круто, да?

Та же ситуация со слабой типизацией. Выбрали слабую типизацию — выбрали, что у тысяч программистов в рантайме одни данные совершенно неожиданно превратятся в другие, что приведёт к непредвиденным последствиям. Так бы и писали: «javascript и php — языки с непредвиденными последствиями».

> parseInt(0.5)
0
> parseInt(0.05)
0
> parseInt(0.005)
0
> parseInt(0.0005)
0
> parseInt(0.00005)
0
> parseInt(0.000005)
0
> parseInt(0.0000005)
5

SQL

SQL создавали, чтобы домохозяйки на естественном языке могли работать с БД. Злая ирония в том, что выражение ниже — максимально естественный язык, который одинаково понимают все люди (в т.ч. таргет-группа — домохозяйки), но при этом SQL работает совершенно не так, как ожидаешь, и удаляет всё к хренам:

DELETE ... WHERE a == 1 OR 2

Вы видите? Сам синтаксис подталкивает сделать эту ошибку.

А ещё SQL позволяет писать текст прямо в запросе, без всякой интерполяции! То есть SELECT one, two, three FROM table. И есть комментарии (--)! И это хорошо, потому что без этих замечательных вещей не было бы SQL инъекций, и жить было бы скучно. А ведь если бы SQL разделял запрос и данные (SELECT %s FROM %s, (name, age), users) и делал бы подстановку именно значений, а не текста, то не было бы инъекций. Но SQL меняться не собирается, и мы изобретаем ORM, которые хоть как-то нас защищают.

rm

Если сделать так, что rm может удалять несколько файлов, раздалённых пробелом, то наверняка в комбинации с подстановкой переменных в Bash получится что-то интересное! :) rm $SOMETHING — никто не знает, что случится.

Но вообще-то можно даже сделать эту ошибку и без подстановки bash — вон, в bumblebee так и получилось. И я считаю, это правильно: если разработчик ставит лишние пробелы по невнимательности, его нужно наказывать максимально жёстко.

Javascript

Если вы выбрали язык, который вместо match_from_beginning() называет метод test()...

> const regexp = /1234/
> regexp.test("12345")
true

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

C / C++

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

Все эти malloc(sizeof), memcpy / strcpy, сами указатели и арифметика с ними и подобное — это пистолет, в котором одно дуло направлено вперёд, а другое назад, прям на вас, и при стрельбе нужно не забывать уворачиваться. А ведь можно не уворачиваться, просто возьмите нормальный пистолет.

Дополнительно в C++ добавили... Нет, не так. В C++ СПЕЦИАЛЬНО добавили случаи, при которых происходит хрен знает что. Не программа падает, нет. ХРЕН ЗНАЕТ ЧТО происходит. Undefined behavior. Собственно, на что был расчёт?

Вы пишете...

int x = 2147483647;  
x = x + 1;

... и C++ это хавает.

Вы пишете...

int arr[3] = {1,2,3};  
int x = arr[5];

... и C++ это хавает.

Он вообще вам не помогает, ему норм и так. Вот вам целый манул по тому, насколько C++ пофиг.

Отступы и форматирование

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

if (condition) do1(); do2()
// или
if (condition)
  do1();
  do2();

Сделайте фигурную скобку { обязательной после if — никто почти и не заметит, но эта ошибка умрёт навсегда, и не будет никаких отключений проверок SSL сертификатов в Apple.

Elasticsearch

Elastic по умолчанию возвращает 10 результатов. Вы не делаете никакие слайсы, он просто тупо возвращает 10 результатов. Это же именно то, что вы хотели при поиске?

Предлагаю такую замечательную идею перенять и сделать, чтобы postgres на каждый запрос возвращала только 10 строк, а ls показывала только 10 файлов. Вот заживём!

Python

Накину на python, раз уж я в нём немного шарю.

create_task

А давайте создадим функцию asyncio.create_task(), чтобы пользователь мог запускать фоновые задачи? Но только если эта задача нигде не хранится, то давайте сборщик мусора её убьёт, ведь это именно то, что хочет пользователь, когда запускает фоновую задачу! И это очевидно, да. Никаких warning о том, что пользователь не присвоил результат никакой переменной, нет. Зачем?

strip

Ещё можно, например, назвать метод strip(...), но вырезать он будет не то, что указано, а каждый элемент того, что указано.

> "surf".strip("suffix")
"r"

При этом в самом языке можно в принципе указать, чтобы функция принимала *args, и тогда это выглядело бы так и работало бы предсказуемо:

> "surf".strip("suffix", "prefix")
"surf"

Да блин, в современном питоне даже есть removeprefix() / removesuffix(), которые делают ровно то, о чём говорят, но strip() всё равно оставили как будто для лулзов, чтобы разрабы писали "Bearer abcdef12345".strip("Bearer ") и удивлялись.

Django

Чтобы секретный ключ гарантированно попал в исходники, в джанго команда создания проекта startproject генерирует секретный ключ и суёт его сразу в код:

SECRET_KEY = "kwdudjeh:uwndy6&jdjdp..."

Больные ублюдки хотят, чтобы ваш ключ утёк.

requests

Если вы откроете документацию по requests, то авторы вам предложат на каждый запрос открывать новое соединение, а потом его закрывать. Самая популярная библиотека для работы по сети в питоне учит делать неэффективно. Красиво? Красиво.

А как делать нормально — скрыто в advanced секции.

❯ Сделай сразу плохо и оставь навсегда

Git

Git сделали настолько неинтуитивным, что в интернете миллион статей, видео и курсов для его изучения. Сама концепция веток и их слияния — замечательная, и понять её никому не составит труда. Но сам интерфейс git... Я даже не буду сильно раскрывать тему, это потянет на отдельную статью и, кажется, на Хабре такая уже была. Поэтому просто накидаю тезисов.

  • git add -p позволяет выбирать, что добавить в staging, но игнорирует новые файлы. Я так миллион раз забывал добавить файлы миграций в коммит

  • git add -p предлагает выбрать / не выбирать в staging целые чанки изменений; если изменения большие, то можно их разбить при помощи s — но до отдельных строчек git разбивать не станет и заставит нажать e, чтобы провалиться в редактор изменений и не суметь сделать ручками правильный diff — по крайней мере, у меня почти никогда не получается

  • git checkout - выбор ветки, git checkout -- . — та же команда, но это откат всех unstaged изменений в папке (но новые файлы останутся)

  • git checkout -- . — откат unstaged изменений, git restore --staged — откат staged изменений (да, совершенно разные команды для похожего функционала)

  • git checkout — выбор ветки, git switch - выбор ветки

  • git push запушит коммит, но не запушит тэг у этого коммита

  • ...

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

Asyncio

Asyncio по умолчанию закладывает, что где-то в асинхронном контексте будет вызвано что-то синхронное, и всё встрянет. Это неизбежно случится.

Asyncio в том виде, как реализовано в питоне, закладывает, что ВСЕ библиотеки будут разбиты на красные/синие. Или разноцветные, но тогда asyncio заставляет разноцветные библиотеки создавать корявые sync_to_async(), syncify() и им подобные. Или же create() и acreate() (django), invoke() + ainvoke() (langchain). В каждой библиотеке свои костыли, порождённые одним решением в дизайне языка.

Javascript

Здесь не про язык, а про сам факт того, что, заходя на сайт, запускается какая-то неизвестная программа. Хм, что может пойти не так? Криптомайнеры в браузере? XSS, CSRF, Clickjacking и ещё хренова туча уязвимостей (в том числе в самих js движках), от которых только и успевают клепать фичи-заплатки? А виноваты мы, обычные разрабы, потому что не включили CORS, CSP, CSRF, secure cookies, не начертили круг мелом вокруг сайта и не станцевали джигу-дрыгу перед продакш сервером. Ага, спасибо.

Система прав в linux

Наверно, для своего времени система пользователей/групп и права rwx были неплохим решением.

Сейчас другое время. Android нам показал, что можно раздавать права на функционал (доступ к камере, доступ к файлам итд). Но в линуксе всё так же любая программа имеет доступ ко всему home, может ходить в сеть и делать вообще что угодно, а права на файлы я всё так же выставляю циферками 0+1+2+4. Любой код из сети может оказаться криптовымог��телем и зашифровать мне все данные или отправить собранные пароли злоумышленнику. И это возможно, потому что система прав это позволяет.

SeLinux, скажете вы. Да, но нужно настраивать и вкуривать — даже если что-то настроено по умолчанию в дистрибутиве, придётся потом настраивать ручками для новых программ. SeLinux хорош тем, что когда я читаю какой-нибудь мануал и вижу секцию «how to make it work with SeLinux», то радуюсь, что мне не нужно её читать и плакать. За это спасибо.

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

Ещё есть пакеты Flatpak, которые изолируют приложение, но вообще-то могут быть созданы кем угодно (где-то читал историю, что в flatpak залили малварь под видом криптокошелька). Или Docker, который изолирует приложение, но поди запусти его с GUI.

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

❯ Вывод

Да, мы делаем ошибки. Да, мы забываем. Но это не наша вина. Так и задумано.

Во всех примерах выше неправильно спрашивать «может ли это привести к ошибке». Правильный вопрос: «когда, где и сколько раз это приведёт к ошибке?»

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

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


Я очень старался с этой статьей, долго собирал факты и ссылки, чтобы показать вам причины и, главное, следствия во всей красе. Надеюсь, вам понравилось. Чтобы не пропустить следующее, приходите в мой канал «Блог погромиста», там я делаю анонсы статей, рассуждаю о тщетности бытия и угораю с него.

Ну а спонсирует мои статьи Timeweb Cloud, за что ему отдельное спасибо. Пользуюсь и рекомендую.


Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале 

Перед оплатой в разделе «Бонусы и промокоды» в панели управления активируйте промокод и получите кэшбэк на баланс.
Перед оплатой в разделе «Бонусы и промокоды» в панели управления активируйте промокод и получите кэшбэк на баланс.
Читайте также:
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
Какой ваш профессиональный уровень / грейд в программировании?
4.25%Отрицание15
7.08%Гнев25
13.6%Торг48
25.5%Депрессия90
49.58%Принятие175
Проголосовали 353 пользователя. Воздержались 36 пользователей.