Русский
Русский
English
Статистика
Реклама

Bash

Перевод В чем именно был смысл xvar xval ?

16.04.2021 12:14:51 | Автор: admin


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

При написании консольных скриптов мы иногда сталкиваемся со сравнениями, в которых каждое значение имеет префикс x. Вот примеры с GitHub:

if [ "x${JAVA}" = "x" ]; thenif [ "x${server_ip}" = "xlocalhost" ]; thenif test x$1 = 'x--help' ; then

Назову этот прием x-hack.

Для любой POSIX-совместимой оболочки значение x-hack будет равно нулю, то есть сравнение в 100% случаев сработает и без x. В чем же тогда его суть?

Ресурсы вроде StackOverflow Q&A размыто поясняют, что это альтернатива цитированию вне контекста, указывающему на проблемы с некоторыми версиями конкретных оболочек или в целом предостерегающему о загадочном поведении, в особенности древних UNIX-систем. Примерами же эти пояснения не подкрепляются.

Чтобы определить, должна ли ShellCheck об этом предупреждать, и если да, то на каком логическом обосновании, я решил обратиться к истории Unix, а именно к архивам Unix Heritage Society. К сожалению, мне не удалось заглянуть в тщательно охраняемый мир подобий HP-UX и AIX, так что пастухам динозавров рекомендую сохранять бдительность.

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

Левая сторона представлена унарным оператором


Оболочка AT&T Unix v6 от 1973 года, по крайней мере согласно данным из PWB/UNIX от 1977 года, проваливала выполнение тестовых команд, где левая сторона была представлена унарным оператором. Это мог заметить любой, кто пытался выполнить проверку параметров командной строки:

% arg="-f"% test "$arg" = "-f"syntax error: -f% test "x$arg" = "x-f"(true)

Ошибка была исправлена в оболочке Борна ОС Unix v7, выпущенной в 1979 году. Тем не менее test и [ были также доступны как отдельные исполняемые файлы, и сохранили вариант ошибочного поведения:

$ arg="-f"$ [ "$arg" = "-f" ](false)$ [ "x$arg" = "x-f" ](true)

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

Современное поведение оболочки Борна в 1988 было скопировано общедоступной KornShell и стало частью POSIX.2 в 1992 году. В GNU Bash 1.14 то же самое было сделано для встроенной инструкции [, при этом пакет GNU shellutils, предоставлявший внешние исполняемые файлы test/[, последовал уже за POSIX. В результате ранние дистрибутивы GNU/Linux вроде SLS этим багом затронуты не были также, как и FreeBSD 1.0.

X-hack в данном случае эффективен по той причине, что ни один унарный оператор не может начинаться с x.

Одна из сторон представлена оператором длины строки -1


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

var="helloworld"[ -l "$var" -gt 8 ] && echo "String is longer than 8 chars"

Согласно приведенному выше обоснованию, он не перешел в POSIX, так как: не был задокументирован в большинстве реализаций, был удален из некоторых реализаций (включая System V), и эта функциональность предоставляется оболочкой. В пример приводится [ ${#var} -gt 8 ].

Это не было проблемой в UNIX v7, где приоритет отдавался =, но в Bash 1.14 от 1996 года данный оператор считывался наперед:

$ var="-l"$ [ "$var" = "-l" ]test: -l: binary operator expected$ [ "x$var" = "x-l" ](true)

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

$ [ "$1" = "-l" -o 1 -eq 1 ][: too many arguments$ [ "x$1" = "x-l" -o 1 -eq 1 ](true)

Позже в том же году этот оператор был удален из Bash 2.0, и проблема ушла вместе с ним.

Левая сторона представлена "!"


Еще одно затруднение в ранних оболочках возникало, когда левая сторона сравнения была представлена оператором отрицания !:

$ var="!"$ [ "$var" = "!" ]test: argument expected            (UNIX v7, 1979)test: =: unary operator expected   (bash 1.14, 1996)(false)                            (pd-ksh88, 1988)$ [ "x$var" = "x!" ](true)

Опять же, x-hack решал проблему, не позволяя распознать ! как оператор отрицания.

Ksh рассматривала его как [ ! "=" ]и игнорировала остальные аргументы. В итоге просто возвращался false, так как = не является нулевой строкой. При этом в ksh завершающие аргументы игнорируются и по сей день:

$ [ -e / random words/ops here ](true)                              (ksh93, 2021)bash: [: too many arguments         (bash5, 2021)

В Bash 2.0 и ksh93 эта проблема в соответствии с POSIX была решена за счет предоставления оператору = приоритета в случае с тремя аргументами.

Левая сторона представлена "("


Это, безусловно, моя любимая.

Встроенная в UNIX v7 оболочка давала сбой, когда левая сторона была представлена левой скобкой:

$ left="(" right="("$ [ "$left" = "$right" ]test: argument expected$ [ "x$left" = "x$right" ](true)

Это происходило из-за того, что ( получал приоритет над = и становился недопустимой группой скобок.

Но почему моя любимая? Вот Dash 0.5.4 вплоть до 2009:

$ left="(" right="("$ [ "$left" = "$right" ][: 1: closing paren expected$ [ "x$left" = "x$right" ](true)

На момент публикации темы в StakOverflow Q&A данный баг продолжал существовать.

Но это еще не все!

Вот Zsh конца 2015 года перед самым выходом версии 5.3:

% left="(" right=")"% [ "$left" = "$right" ](true)% [ "x$left" = "x$right" ](false)

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

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

В запоздавших можно также записать Solaris, чья /bin/sh оставалась устаревшей оболочкой Борна даже в Solaris 10 2009 года. Однако причиной такой задержки определенно стала совместимость, а не оценка разработчиками этой оболочки как оптимальной. Совместимая со стандартами оболочка оставалась опцией достаточно долго, пока Solaris 11 не перетащил ее в 21 век или как минимум в 90-е переключившись на ksh93 по умолчанию в 2011 году.

X-hack выручает во всех подобных случаях, не давая распознать операнды как скобки.

Заключение


Прием с добавлением x действительно был полезен и успешно исправлял ряд реальных практических проблем в нескольких оболочках.

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

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

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

Эпилог


Проблема с [ "(" = ")" ] в Dash была впервые отмечена в 2008 году и проявлялась как в Bash 3.2.48, так и в Dash 0.5.4. В bash на macOS ее можно встретить до сих пор:

$ str="-e"$ [ \( ! "$str" \) ][: 1: closing paren expected     # dashbash: [: `)' expected, found ]   # bash

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

Мейнтейнер Dash, Герберт Сюй, по этому поводу оставил в исправлении такой комментарий:

/* * Регламент POSIX: написавший это заслуживает Нобелевской премии мира* */
Подробнее..

Перевод Генерация изображений с помощью echoprintf в 5 строчках кода без библиотек и заголовков

25.04.2021 12:10:20 | Автор: admin
tl;dr: форматы файлов Netpbm позволяют легко выводить пиксели, используя только текстовый ввод-вывод.



Вот весь генерирующий это изображение скрипт bash без зависимостей:

#!/bin/bashexec > my_image.ppm    # Все инструкции echo будут писать в этот файлecho "P3 250 250 255"  # формат, ширина, высота, максимальное значение цветаfor ((y=0; y<250; y++)) {  for ((x=0; x<250; x++)) {    echo "$((x^y)) $((x^y)) $((x|y))" # r, g, b  }}

Это все, что нужно для генерации изображения, которое можно будет считать стандартными инструментами вроде GIMP, ImageMagick и Netpbm.

Дополню, что иногда удобно выводить изображение для отладки 2D-алгоритмов с помощью printf, визуализации данных или просто для отображения на экране процедурных пикселей.

Тем не менее если изначально не знать о примере выше, то порог для реализации вывода графики может показаться достаточно высоким. Даже если использовать библиотеку single_file, это будет означать необходимость настройки еще одного компонента. И это особенно неприятно в случае отладки, когда знаешь, что буквально в течение часа его удалишь.

К счастью, пакет инструментов Netpbm предлагает на удивление гибкое решение: набор форматов файлов с наименьшим общим знаменателем для полноцветных Portable PixMaps (PPM), Portable GreyMaps (PGM) и монохромных Portable BitMaps (PBM), которые все можно записать в виде простого текста ASCII через базовый ввод-вывод любого языка.

Все вместе эти форматы известны как PNM: Portable aNyMaps.

Вышеприведенного скрипта bash вполне достаточно для начала, тем не менее подробное описание этого формата файлов можно найти в man ppm, man pgm и man pbm в системе, где установлен Netpbm.

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

Для преобразования их в более распространенные форматы нужно либо выполнить экспорт в GIMP, либо использовать команду ImageMagick convert my_file.ppm my_file.png, либо команду NetPBM pnmtopng < my_file.ppm > my_file.png.

Если вы решите передать изображения, используя этот простой формат ASCII, то команда NetPBM pnmtoplainpnm преобразует двоичный ppm/pgm/pbm (создаваемый любым инструментом, включая anytopnm из Netpbm) в ASCII ppm/pgm/pbm.

Если вы захотите поэкспериментировать с каким-либо алгоритмом обработки изображений, то можете легко задействовать прекрасный набор инструментов Netpbm путем чтения/записи PPM через stdin/stdout:

curl http://example.com/input.png |     pngtopnm |     ppmbrighten -v +10 |    yourtoolhere |    pnmscale 2 |    pnmtopng > output.png

Подробнее..

Перевод Почему usrbintest на 4Кб меньше, чем usrbin?

25.04.2021 16:15:44 | Автор: admin


Пользователь с Reddit под ником mathisweirdaf поделился интересными наблюдениями:

 $ ls -lh /usr/bin/{test,[}-rwxr-xr-x 1 root root 59K  Sep  5  2019 '/usr/bin/['-rwxr-xr-x 1 root root 55K  Sep  5  2019  /usr/bin/test

[ и test должны быть псевдонимами друг друга, и все же между исполняющими их файлами из GNU coreutils наблюдается разница в 4Кб. Почему?

Во-первых, для всех, кого это удивило: да, существует /usr/bin/[. По этой теме у меня есть отдельная статья (англ.), но я все же коротко поясню:

Когда вы пишите if [ -e /etc/passwd ]; then .. эта скобка выступает не синтаксисом оболочки, а просто стандартной командой с забавным именем. Обычно она встроена в оболочку, но иногда может реализовываться через /usr/bin/[. Это объясняет многое из ее загадочного поведения, например, почему она чувствительна к пробелам: [1=2] оказывается не более валидно, чем ls-l/tmp.

Тем не менее откуда возникает разница в размере? Можно сравнить вывод objdump, чтобы увидеть, куда помещаются данные. Вот выдержка из objdump -h /usr/bin/[:

              size                                          offset15 .text         00006e82  0000000000002640  0000000000002640  00002640  2**416 .fini         0000000d  00000000000094c4  00000000000094c4  000094c4  2**217 .rodata       00001e4c  000000000000a000  000000000000a000  0000a000  2**5

А вот objdump -h /usr/bin/test:

15 .text         000068a2  0000000000002640  0000000000002640  00002640  2**416 .fini         0000000d  0000000000008ee4  0000000000008ee4  00008ee4  2**217 .rodata       00001aec  0000000000009000  0000000000009000  00009000  2**5

Здесь мы видим, что сегмент .text (скомпилированный исполняемый код) на 1504 байта больше, в то время как .rodata (постоянные значения и строки) больше на 864 байта.

Суть в том, что увеличенный размер сегмента .text вынуждает его перемещаться из 8000 в 9000, пересекая границу размера страницы 0х1000 (4096) и, следовательно, смещая все другие сегменты на 4096 байтов. Именно эту разницу в размере мы и наблюдаем.

Единственное номинальное отличие между [ и test в том, что [ требует наличия ] в качестве заключительного аргумента. Проверка этого потребовала бы минимальное количество кода, так зачем все же используются те самые ~1500 байтов?

Поскольку сложно просматривать отделенные исполнительные файлы, я создал собственную копию coreutils и сравнил список функций в каждом:

$ diff -u <(nm -S --defined-only src/[ | cut -d ' ' -f 2-) <(nm -S --defined-only src/test | cut -d ' ' -f 2-)--- /dev/fd/63      2021-02-02 20:21:35.337942508 -0800+++ /dev/fd/62      2021-02-02 20:21:35.341942491 -0800@@ -37,7 +37,6 @@ D __dso_handle d _DYNAMIC D _edata-0000000000000099 T emit_bug_reporting_address B _end 0000000000000004 D exit_failure 0000000000000008 b file_name@@ -63,7 +62,7 @@ 0000000000000022 T locale_charset 0000000000000014 T __lstat 0000000000000014 t lstat-0000000000000188 T main+00000000000000d1 T main 000000000000000b T make_timespec 0000000000000004 d nslots 0000000000000022 t one_argument@@ -142,16 +141,10 @@ 0000000000000032 T umaxtostr 0000000000000013 t unary_advance 00000000000004e5 t unary_operator-00000000000003d2 T usage+0000000000000428 T usage 0000000000000d2d T vasnprintf 0000000000000013 T verror 00000000000000ae T verror_at_line-0000000000000008 D Version-00000000000000ab T version_etc-0000000000000018 T version_etc_ar-000000000000042b T version_etc_arn-000000000000002f R version_etc_copyright-000000000000007a T version_etc_va 000000000000001c r wide_null_string.2840 0000000000000078 T x2nrealloc 000000000000000e T x2realloc

Главные участники это функции version_etc*. Что они делают?

Давайте посмотрим:

/* The three functions below display the --version information the   standard way [...]

Это 260 строк развернутых, интернационализированных, условных способов форматирования данных, которые составляют вывод --version. Все вместе они занимают около bc <<< "ibase=16; 7A+2F+42B+18+AB+8+99" = 1592 байтов.

Что это значит? Все просто. Дополнительные 4Кб уходят вот на что:

$ /usr/bin/[ --version[ (GNU coreutils) 8.30Copyright (C) 2018 Free Software Foundation, Inc.License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>.This is free software: you are free to change and redistribute it.There is NO WARRANTY, to the extent permitted by law.Written by Kevin Braunsdorf and Matthew Bradburn.

[ --version недостает заключительной ], поэтому вызов оказывается недействительным, и результат определяется реализацией. GNU спокойно позволяет вывести информацию о версии.

Тем временем /usr/bin/test --version оказывается действительным вызовом, и POSIX предписывает, чтобы она возвращала успех, когда первый параметр (--version) является не пустой строкой.

Эта разница даже упоминается в документации:

Примечание: [ отвечает за опции --help и --version, а test нет.test рассматривает каждую из них просто как непустую строку.

Загадка разгадана!

(Задачка: каковы будут последствия, если вопреки POSIX test будет поддерживать --help и --version?)

Подробнее..

Перевод var? и ampamp два простых помощника в работе с командами оболочки из документации

29.04.2021 18:18:03 | Автор: admin


tl;dr: используйте инструкцию ${placeholders?} для выдачи ошибки при пропуске параметра и объединяйте команды оболочки с помощью &&, чтобы упростить и обезопасить их копирование из технической документации.

Я часто обращаюсь к документации, откуда копирую команды оболочки. К примеру, для установки JDK это может выглядеть так:

sudo apt-get updatesudo apt-get install openjdk-<VERSION>-jdk

Очевидно, что этот пример прост и понятен. Более реалистичный же случай может содержать команды из 160+ символов, вызывающие незнакомые инструменты и скрипты утилит со множеством фиксированных и переменных параметров. Многие подобные примеры вы без проблем найдете во внутренней технической документации вашей компании.

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

Используйте ${NAME?} вместо специальных плейсхолдеров вроде NAME или <NAME>


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

$ sudo apt-get install openjdk-${VERSION?}-jdkbash: VERSION: parameter not set

(Это будет работать при условии отсутствия переменной среды с тем же именем).

Сравните предложенный вариант с оригинальными угловыми скобками, которые в лучшем случае дадут сбой с неверным указанием ошибки:

$ sudo apt-get install openjdk-<VERSION>-jdkbash: VERSION: No such file or directory

Или приведут к созданию/усечению файлов, давая сбивающие с толку ошибки (если вообще повезет их получить):

$ sudo apt-get install openjdk-<VERSION>-jdkE: Unable to locate package openjdk$ grep VERSION *grep: invalid option -- 'j'

Здесь перенаправление >-jdk было интерпретировано как перенаправление файла (также как в echo Hi > foo.txt). В результате был создан файл -jdk, что привело к неожиданному сбою корректных команд с globs (а представьте, что бы произошло с grep <alias name> ~/.bashrc!).

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

Используйте && между последовательными командами


Использование && для объединения нескольких последовательных команд дает даже не одно, а два преимущества:

sudo apt-get update &&sudo apt-get install openjdk-${VERSION?}-jdk

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

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

$ sudo apt-get update[sudo] password for vidar:Sorry, try again.[sudo] password for vidar:

Я вставил две команды, но первой требуется пароль. Далее в качестве пароля была считана вторая команда, что привело к ошибке "Sorry, try again".

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

Сравните этот вариант с использованием &&, когда оболочка терпеливо считывает все команды заранее (за счет указателя на продолжение >) и только затем начинает их выполнять:

$ sudo apt-get update &&> sudo apt-get install openjdk-14-jdk[sudo] password for vidar:

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

Заключение


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

Подробнее..

Перевод Неожиданные подвохи при перенаправлениях оболочки в ((i))

04.05.2021 16:09:09 | Автор: admin


Год назад вышла ShellCheck v0.7.1. Главным образом в ней были подчищены и исправлены имеющиеся проверки, но также появились и новые. Лично меня из всех новинок больше всех удивила та, что указывает на проблему, обсуждение которой я еще нигде не встречал:

In demo line 6:  cat template/header.txt "$f" > archive/$((i++)).txt                                             ^  SC2257: Arithmetic modifications in command redirections          may be discarded. Do them separately. #Арифметические изменения при перенаправлениях в command могут быть #отброшены. Выполняйте их отдельно

А вот весь скрипт:

#!/bin/bashi=1for f in *.txtdo  echo "Archiving $f as $i.txt"  cat template/header.txt "$f" > archive/$((i++)).txtdone

Опытные сценаристы уже наверняка забежали вперед и повторили это в своей оболочке, выяснив, что изменение будет работать, по крайней мере в Bash 5.0.16(1):

bash-5.0$ i=0; echo foo > $((i++)).txt; echo "$i" 1

Исходя из этого вы можете ожидать беглого просмотра истории коммитов Bash, и, быть может, призыва сохранять благосклонность к нашим обездоленным собратьям на macOS, использующим Bash 3.

Но нет. Вот демо-скрипт на той же системе:

bash-5.0$ ./demoArchiving chocolate_cake_recipe.txt as 1.txtArchiving emo_poems.txt as 1.txtArchiving project_ideas.txt as 1.txt

То же самое верно для source ./demo, которая выполняет скрипт в том же экземпляре оболочки, где мы только что проводили проверку. Более того, происходит это только при перенаправлениях, но не в аргументах.

Так в чем же здесь дело?

Оказывается, что Bash, Ksh и BusyBox ash в процессе установки файловых дескрипторов также расширяют имя файла, из которого происходит перенаправление. Если вы знакомы с моделью процессов Unix, то псевдокод будет выглядеть так:

if command is external:  fork child process:    filename := expandString(command.stdout) # инкрементно увеличивает i    fd[1] := open(filename)    execve(command.executable, command.args)else:  filename := expandString(command.stdout)   # инкрементно увеличивает i  tmpFd := open(filename)  run_internal_command(command, stdout=tmpFD)  close(tmpFD)

Говоря иначе, область изменения переменной зависит от того, произвела ли оболочка ответвление нового процесса в ожидании выполнения команды.

Для встроенных команд, которые не разветвляются, например echo, это означает, что изменение произойдет в текущей оболочке. Именно такой тест мы и провели.

Для внешних же команд вроде cat изменение видимо только между моментом установки файлового дескриптора и вызовом команды для выполнения процесса. Это и делает демо-скрипт.

Конечно же, подоболочки хорошо известны опытным программистам, а также описаны в статье Why Bash is like that: Subshells. Но лично для меня это новый и, в частности, коварный их источник.

К примеру, этот скрипт отлично работает в busybox sh, где cat является встроенной:

$ busybox sh demoArchiving chocolate_cake_recipe.txt as 1.txtArchiving emo_poems.txt as 2.txtArchiving project_ideas.txt as 3.txt

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

awk() { gawk "$@"; }# Инкрементируетawk 'BEGIN {print "hi"; exit;}' > $((i++)).txt# Не инкрементируетgawk 'BEGIN {print "hi"; exit;}' > $((i++)).txt  

Либо, если вы хотите переопределить псевдоним, то результат будет зависеть от того, использовали ли вы command или \:

# Инкрементируетcommand git show . > $((i++)).txt# Не инкрементирует\git show . > $((i++)).txt

Чтобы избежать этой путаницы, обратите внимание на совет ShellCheck, и если при перенаправлении переменная является частью имени файла, то просто увеличивайте ее отдельно:

anything > "$((i++)).txt": $((i++))

Выражаю благодарность Strolls из #bash@Freenode за то, что указал на это поведение.

P.S. В процессе подготовки материала для статьи я выяснил, что dash всегда производит увеличение (хоть и с помощью $((i=i+1)), так как не поддерживает ++). ShellCheck v0.7.1 по-прежнему делает предупреждение, а код из master-ветки этого уже не делает.

Подробнее..

Как портировать SDK Flutter на ТВ-приставку для разработки и запуска приложений Android TV

15.04.2021 16:11:54 | Автор: admin

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

Учитывая, что программный стек RDK или Reference Design Kit сейчас активно используется для разработки OTT-приложений, голосового управления приставками и других продвинутых функций для видео по запросу (VoD), мы хотели разобраться, сможет ли Flutter работать на ТВ-приставке. Оказалось, что да, но, как это обычно бывает, есть нюансы.

Далее мы по шагам распишем процесс портирования и запуска Flutter на встраиваемых Linux-платформах и разберемся, как этот SDK с открытым исходным кодом от Google чувствует себя на железе с ограниченными ресурсами и ARM-процессорами.

Но прежде чем переходить непосредственно к Flutter и его преимуществам скажем пару слов об исходном решении, которое было задействовано на ТВ-приставке. На плате работала связка набор библиотек EFL + протокол Wayland, а рисование примитивов было реализовано из node.js на основе плагинного нативного модуля. Это решение неплохо себя показало с точки зрения производительности при отображении кадров, однако сам EFL отнюдь не самый новый фреймворк для отрисовки. А в режиме выполнения node.js со своим огромным event-loopом казался уже не самой перспективной идеей. В то же время Flutter мог позволить нам задействовать более производительную связку рендеринга.

Для тех, кто не в теме: первую версию этого SDK с открытым кодом Google представил еще шесть лет назад. Тогда этот набор средств разработки годился только для Android. Сейчас на нем можно писать приложения для веба, iOS, Linux и даже Google Fuchsia. :-) Рабочий язык для разработки приложений на Flutter Dart, в свое время он был предложен в качестве альтернативы JavaScript.

Перед нами стоял вопрос: даст ли переход на Flutter какой-то выигрыш по производительности? Ведь подход там совершенно иной, хоть в конечном счете и имеется та же графическая подсистема Wayland + OpenGL. Ну и как там с поддержкой процессоров с neon-инструкциями? Были и другие вопросы, например, нюансы по переносу UI на dart или то, что поддержка Linux находится в стадии альфы-беты.

Сборка Flutter Engine для ТВ-приставок на базе ARM

Итак, начнем. Вначале Futter нужно запустить на чужеродной платформе с Wayland + OpenGL ES. В основе рендеринга у Flutter лежит библиотека Skia, которая прекрасно поддерживает OpenGL ES, поэтому в теории все выглядело хорошо.

При сборке Flutter под наши целевые устройства (три ТВ-приставки с RDK), к нашему удивлению, проблемы возникли только на одной. Не будем с ней сражаться, т.к. из-за старой архитектуре intel x86 она для нас не является приоритетной. Лучше сосредоточимся на оставшихся двух ARM-платформах.

Вот, с какими опциями мы собирали Flutter Engine:

./flutter/tools/gn \      --embedder-for-target \      --target-os linux \      --linux-cpu arm \      --target-sysroot DEVICE_SYSROOT      --disable-desktop-embeddings \      --arm-float-abi hard      --target-toolchain /usr      --target-triple arm-linux-gnueabihf      --runtime-mode debugninja -C out/linux_debug_unopt_arm

Большинство опций понятны: собираем под 32-битный ARM-процессор и Linux, выключая при этом все лишнее через --embedder-for-target --disable-desktop-embeddings.

Для сборки в системе должен быть установлен clang версии 9 и выше, т.е. это стандартный сборочный механизм Flutter, инструментарий кросс-компиляции gcc не пойдет. Самое важное подать корректный target-sysroot устройства с RDK.

Честно говоря, мы удивились, что при сборке не возникло вообще никаких нюансов. На выходе получаем заветную библиотеку flutter_engine.so и заголовок с необходимыми функциями для эмбеддера.

Теперь можно собрать целевой проект flutter/dart с нашей библиотекой/движком. Это сделать легко:

flutter --local-engine-src-path PATH_TO_BUILDED_ENGINE_src --local-engine=host_debug_unopt build bundle

Важно! Сборка проекта должна происходить не на устройстве с собранной библиотекой, а на хостовой, т.е. x86_64!

Для этого достаточно еще раз пройти путь сборкой gn и ninja только под x86_64! Именно она указывается в параметре host_debug_unopt.

PATH_TO_BUILDED_ENGINE_src это путь, где находится engine/src/out.

За запуск Flutter Engine под системой обычно отвечает embedder, именно он конфигурирует Flutter под целевую систему и дает основные контексты рендеринга библиотеке Skia и Dart-обработчику. Не так давно в состав Flutter добавили linux-embedder, и, в частности, GTK-embedder, так что можно воспользоваться им из коробки. На нашей платформе на момент портирования это был не вариант, нужно было что-то независимое от GTK.

Рассмотрим некоторые особенности реализации, которые пришлось учесть с кастомным эмбеддером (все, кто любит разбирать не нюансы, а исходники целиком, может сразу перейти к проекту нашего форка с доработками на github.com). К тому же по производительности наш вариант немного выигрывал у версии GTK, что было крайне важно для заказчика, и не тянул за собой весь зоопарк GTK-библиотек.

Так что же вообще нужно от эмбеддера для запуска flutter-приложения? Достаточно, чтобы он просто вызывал из библотеки flutter_engine.so

FlutterEngineRun(FLUTTER_ENGINE_VERSION, &config, &args, display /* userdata */, &engine_);

где в качестве параметров идет передача настроек проекта (директория с собранным flutter bundle) FlutterProjectArgs args и аргументов рендеринга FlutterRendererConfig config.

В первой структуре как раз задается путь bundle-пакета, собранного flutter-утилитой, а во второй используются контексты OpenGL .

// пример использования на github.com/DEgITx/flutter_wayland/blob/master/src/flutter_application.cc

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

Проблемы и их решение

Теперь поговорим о нюансах, с которыми мы столкнулись на этапе портирования. А как же без них? Не только ведь библиотеки собирать :-)

1. Краш эмбеддера и замена очередности вызова функций

Первая проблема, с которой мы столкнулись краш эмбеддера под платформой. Казалось бы, инициализация egl-контекста в других приложения происходит нормально, FlutterRendererConfig инициализирован корректно, но нет эмбеддер не заводится. Значит в связке что-то явно не так. Оказалось, eglBindAPI нельзя вызывать перед eglGetDisplay, на котором происходит особая инициализация nexus-драйвера дисплея (у нас платформа базируется на чипе BCM). В обычном Linux это не проблема, но на целевой платформе оказалась иначе.

Корректная инициализация эмбеддера выглядит так:

egl_display_ = eglGetDisplay(display_);if (egl_display_ == EGL_NO_DISPLAY) {  LogLastEGLError();  FL_ERROR("Could not access EGL display.");  return false;}if (eglInitialize(egl_display_, nullptr, nullptr) != EGL_TRUE) {  LogLastEGLError();  FL_ERROR("Could not initialize EGL display.");  return false;}if (eglBindAPI(EGL_OPENGL_ES_API) != EGL_TRUE) {  LogLastEGLError();  FL_ERROR("Could not bind the ES API.");  return false;}

// github.com/DEgITx/flutter_wayland/blob/master/src/wayland_display.cc корректная реализация, т.е. помогла измененная очередность вызова функций.

Теперь, когда нюанс запуска улажен, мы рады увидеть заветное демо-окно приложения на экране :-).

2. Оптимизация производительности

Настало время проверить производительность. И, честно говоря, она нас не сильно порадовала в режиме отладки (debug mode). Что-то работало шустро, что-то наоборот, имело большие просадки по фреймам и тормозило гораздо больше, чем что-то похожее на EFL+Node.js.

Мы немного расстроились и начали копать дальше. В SDK Flutter есть специальный режим компиляции машинного кода AOT, это даже не jit, а именно компиляция в нативный код со всеми сопутствующими оптимизациями, именно это подразумевается под по релиз-версией Flutter. Такой поддержки у нас в эмбеддере пока не было, добавляем.

Необходимы определенные инструкции, поданные аргументами к FlutterEngineRun

// полная реализация github.com/DEgITx/flutter_wayland/blob/master/src/elf.cc

vm_snapshot_instructions_ = dlsym(fd, "_kDartVmSnapshotInstructions");if (vm_snapshot_instructions_ == NULL) {  error_ = strerror(errno);  break;}vm_isolate_snapshot_instructions_ = dlsym(fd, "_kDartIsolateSnapshotInstructions");if (vm_isolate_snapshot_instructions_ == NULL) {  error_ = strerror(errno);  break;}vm_snapshot_data_ = dlsym(fd, "_kDartVmSnapshotData");if (vm_snapshot_data_ == NULL) {  error_ = strerror(errno);  break;}vm_isolate_snapshot_data_ = dlsym(fd, "_kDartIsolateSnapshotData");if (vm_isolate_snapshot_data_ == NULL) {  error_ = strerror(errno);  break;}
if (vm_snapshot_data_ == NULL || vm_snapshot_instructions_ == NULL || vm_isolate_snapshot_data_ == NULL || vm_isolate_snapshot_instructions_ == NULL) {  return false;}*vm_snapshot_data = reinterpret_cast <  const uint8_t * > (vm_snapshot_data_);*vm_snapshot_instructions = reinterpret_cast <  const uint8_t * > (vm_snapshot_instructions_);*vm_isolate_snapshot_data = reinterpret_cast <  const uint8_t * > (vm_isolate_snapshot_data_);*vm_isolate_snapshot_instructions = reinterpret_cast <  const uint8_t * > (vm_isolate_snapshot_instructions_);
FlutterProjectArgs args;// передаем все необходимое в argsargs.vm_snapshot_data = vm_snapshot_data;args.vm_snapshot_instructions = vm_snapshot_instructions;args.isolate_snapshot_data = vm_isolate_snapshot_data;args.isolate_snapshot_instructions = vm_isolate_snapshot_instructions;

Теперь, когда все есть, нужно собрать приложение особым образом, чтобы получить AOT-скомпилированный модуль под целевую платформу. Это можно сделать, выполнив команду из корня dart-проекта:

$HOST_ENGINE/dart-sdk/bin/dart \--disable-dart-dev \$HOST_ENGINE/gen/frontend_server.dart.snapshot \--sdk-root $DEVICE_ENGINE}/flutter_patched_sdk/ \--target=flutter \-Ddart.developer.causal_async_stacks=false \-Ddart.vm.profile=release \-Ddart.vm.product=release \--bytecode-options=source-positions \--aot \--tfa \--packages .packages \--output-dill build/tmp/app.dill \--depfile build/kernel_snapshot.d \package:lib/main.dart$DEVICE_ENGINE/gen_snapshot                               \    --deterministic                                             \    --snapshot_kind=app-aot-elf                                 \    --elf=build/lib/libapp.so                                   \    --no-causal-async-stacks                                    \    --lazy-async-stacks                                         \    build/tmp/app.dill

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

-Ddart.vm.profile=release \
-Ddart.vm.product=release \

указывают на то, что нам не нужен профайлер в комплекте и у нас продуктовая версия.

output-dill нужен для построения нативной библитеки libapp.so.

Самыми важными для нас являются пути $DEVICE_ENGINE и $HOST_ENGINE два собранных движка под целевую (ARM) и хост-системы (x86_64) соответственно. Тут важно ничего не перепутать и убедиться, что libapp.so получается именно 32-битной ARM-версией:

$ file libapp.so libapp.so: ELF 32-bit LSB shared object, ARM, EABI5 version 1 (SYSV), dynamically linked

Запускаем и-и-и-и... вуаля! все работает!

И работает шустрее значительно! Теперь уже можно говорить о сравнимой производительности и эффективности рендеринга с исходным приложением на базе набора библиотек EFL. Рендеринг работает почти без запинки и почти идеально на простых приложениях.

3. Подключение устройств ввода

В рамках этой статьи мы пропустим то, как подружили Wayland и эмбеддер с пультом, мышкой и другими устройствами ввода. На их реализацию можно посмотреть в исходниках эмбеддера.

4. Интерфейс на ТВ-приставке под Linux и Android и как увеличить производительность в 23 раза

Коснемся еще нескольких нюансов производительности, с которыми столкнулись в продуктовом UI-приложении. Нас очень обрадовала идентичность работы UI как на целевом устройстве, так и на Linux и Android. Уже сейчас Flutter может вполне может похвастаться очень гибкой портируемостью.

Еще отметим интересный опыт оптимизации самого dart-приложения под целевую платформу. Нас разочаровала довольно низкая производительность продуктового приложения (в отличии от демок). Мы взяли в руки профайлер и начали копать идовольно быстро обнаружили активное использование функций __brcm_cpu_dcache_flush и khrn_copy_8888_to_tf32 во время анимаций (на платформе используется чип процессора Broadcom/BCM ). Явно происходило какое-то очень жесткое пиксельное программное трансформирование или копирование во время анимаций. В итоге виновник был найден: в одной из панелей был задействован эффект размытия:

//...filter: new ImageFilter.blur(sigmaX: 10.0, sigmaY: 10.0),//...

Комментирование одного этого эффекта дало увеличение производительности приложения в дватри раза и вывело приложение на стабильные 50-60 fps. Это хороший пример того, как один специфический эффект может уничтожить производительность во всем приложении на Flutter при том, что он был зайствован всего-лишь в одной панели, которая большую часть времени была скрыта.

Итого

В результате мы получили не просто работающее продуктовое приложение, а работающее приложение с качественным фреймрейтом на Flutter на целевом устройстве. Форк и наша версия эмбеддера под RDK и другие платформы на основе Wayland находится тут: github.com/DEgITx/flutter_wayland

Надеемся, опыт нашей команды в разработке и портировании ПО для ТВ-приставок и Smart TV пригодится вам в своих проектах и послужит отправной точкой для портирования Flutter на других устройствах.

[!?] Вопросы и комментарии приветствуются. На них будет отвечать автор статьи Алексей Касьянчук, наш инженер-программист

Подробнее..

Стенды разработки без очередей и простоев

23.03.2021 08:12:20 | Автор: admin

Цель статьи - показать один из возможных подходов для организации гибкого развёртывания dev/test стендов. Показать какие преимущества предоставляет нам IaC подход в сочетании с современными инструментами.


Предыстория

Имеется несколько стендов для разработчиков - devs, tests, production. Новые версии компонентов продукта появляются несколько раз в день.

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

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

Задача

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

Стек

Gitlab CI, Terraform, Bash, любое приватное/публичное облако.

Технические сложности:

  1. Terraform state file - из коробки у нас нет возможности использовать переменную в значенииимени state файла. Нужно что-то придумывать или использовать другой продукт.

  2. Subnets - каждое новое окружение должно быть создано в изолированной подсети. Нужно контролировать свободные/занятые подсети, некий аналог DHCP, но для подсетей.

Алгоритм

  1. Gitlab CI выполняет pipeline. Связывает все остальные компоненты воедино.

  2. Terraform создаёт и удаляет экземпляры виртуальных машин.

  3. Configuration manager(CM) - разворачивает сервисы.

  4. Bash сценарии подготавливают конфигурационные файлы специфичные для каждого стенда.

Структура репозитория

development-infrastructure/  deploy/    env1/      main.tf      backend.tf     ansible-vars.json       subnets.txt     env2/    ...  cm/    ...  modules/    azure/      main.tf      variables.tf  scripts/    env.sh    subnets.txt   .gitlab-ci.yml
  • deploy - содержит специфичные для каждого окружения файлы - файл с переменными для terraform и CM, файл содержащий адрес подсети стенда.

  • cm - в моём случае, тут хранятся Ansible плейбуки для настройки ОС и разворачивания сервисов.

  • modules - модули terraform которые будут получать в качестве параметров имя окружения и адрес подсети

  • scripts - bash сценарии для создания и удаления стендов и их конфигураций

.gitlab-ci.yml:

stages:  - create environment  - terraform apply  - cm  - destroy environment .template: variables: ENV: $NAME_ENV  when: manual tags: [cloudRunner01]  only:   refs:    - triggers Create environment:  stage:create environment  extends: .template script:   - ./scripts/create_env.sh -e $ENV -a create  artifacts:   paths:    - deploy/${ENV}/backend.tf    - deploy/${ENV}/main.tf    - deploy/${ENV}/vars.json Create instances:  stage: terraform apply extends: .template  script:   - cd ./deploy/$ENV   - terraform init -input=false   - terraform validate   - terraform plan -input=false -out=tf_plan_$ENV   - terraform apply -input=false tf_plan_$ENV Deploy applications:  stage: cm  extends: .template  script:   - # мы можем передать имя окружения в качестве параметра нашей CM   - # в моём случае, на основе переменной $ENV генерируются сертификаты,   - # конфигурируется обратный прокси сервер и т.п.   - # также мы можем использовать данные из terraform Destroy instances and environment:  stage:destroy environment  extends: .template  script:   - cd ./deploy/$ENV   - terraform init -input=false   - terraform destroy -auto-approve   - ./scripts/delete_env.sh -e $ENV -a delete 

Остановимся подробнее на каждом шаге нашего пайплайна:

  • Create environment - на основе имени окружения, полученного из переменно NAME_ENV, создаём уникальные для окружения файлы, после чего помещаем их в наш git репозиторий.

  • Create instances - создаём инстансы(виртуальные машины) и подсети, которые будут использоваться окружением.

  • Deploy applications - разворачиваем наши сервисы с помощью любимого Configuration Manager.

  • Destroy instances and environment - с помощью bash сценария данный шаг удалит наши инстансы, после чего удалит из репозитория каталог с файлами окружения. Освободившаяся подсеть будет возвращена в файл scripts/subnets.txt.

Запуск пайплайна происходит с объявлением переменной NAME_ENV, содержащей имя нового стенда:

Разработчики не имеют доступа к Git репозиторию и могут вносить в него изменения только через запуск pipeline.

modules/base/main.tf:

# лучшие практики и личный опыт настоятельно рекомендуют фиксировать версию провайдера provider "azurerm" { version = "=1.39.0" }  # создаём новую группу ресурсов, это особенность Azure. Имя группы уникально, в этом случае будет удобно использовать имя окружения resource "azurerm_resource_group" "product_group" { name= "${var.env_name}" location = "East Europe" } # создаем сеть resource "azurerm_virtual_network" "vnet" { name= "product-vnet" resource_group_name = azurerm_resource_group.product_group.name location= azurerm_resource_group.product_group.location address_space= [var.vnet_address] } #используем подсеть полученную с помощью bash сценария resource "azurerm_subnet" "subnet" { name= "product-subnet" resource_group_name= azurerm_resource_group.product_group.name virtual_network_name = azurerm_virtual_network.vnet.name address_prefix= var.subnet_address } # теперь можем создать виртуальную машинуresource "azurerm_virtual_machine" "product_vm" { name= "main-instance" location= azurerm_resource_group.product_group.location resource_group_name= azurerm_resource_group.product_group.name network_interface_ids = [azurerm_network_interface.main_nic.id]   } # прочие ресурсы и виртуальные машины... 

Чтобы сократить листинг я убрал большую часть обязательных, но, в нашем случае, неважных ресурсов.

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

scripts/env.sh:

#!/bin/bash set -eu CIDR="24" DEPLOY_DIR="./deploy" SCRIPT_DIR=$(dirname "$0") usage() { echo "Usage: $0 -e [ENV_NAME] -a [create/delete]" echo "-e: Environment name" echo "-a: Create or delete" echo "-h: Help message" echo "Examples:" echo "$0 -e dev-stand-1 -a create" echo "$0 -e issue-1533 -a delete" } while getopts 'he:a:' opt; do case "${opt}" in e) ENV_NAME=$OPTARG ;; a) ACTION=$OPTARG ;; h) usage; exit 0 ;; *) echo "Unknown parameter"; usage; exit 1;; esac done if [ -z "${ENV_NAME:-}" ] && [ -z "${ACTION:-}" ]; then usage exit 1 fi # приводим имя окружения к нижнему регистру ENV_NAME="${ENV_NAME,,}" git_push() { git add ../"${ENV_NAME}" case ${1:-} in create) git commit -am "${ENV_NAME} environment was created" git push origin HEAD:"$CI_COMMIT_REF_NAME" -o ci.skip echo "Environment ${ENV_NAME} was created.";; delete) git commit -am "${ENV_NAME} environment was deleted" git push origin HEAD:"$CI_COMMIT_REF_NAME" -o ci.skip echo "Environment ${ENV_NAME} was deleted.";; esac } create_env() { # создаём каталог для нового окружения if [ -d "${DEPLOY_DIR}/${ENV_NAME}" ]; then echo "Environment ${ENV_NAME} exists..." exit 0 else mkdir -p ${DEPLOY_DIR}/"${ENV_NAME}" fi # получаем адрес подсети NET=$(sed -e 'a$!d' "${SCRIPT_DIR}"/subnets.txt) sed -i /"$NET"/d "${SCRIPT_DIR}"/subnets.txt echo "$NET" > ${DEPLOY_DIR}/"${ENV_NAME}"/subnets.txt if [ -n "$NET" ] && [ "$NET" != "" ]; then echo "Subnet: $NET" SUBNET="${NET}/${CIDR}" else echo "There are no free subnets..." rm -r "./${DEPLOY_DIR}/${ENV_NAME}" exit 1 fi   pushd "${DEPLOY_DIR}/${ENV_NAME}" || exit 1 # Создаем main.tf terraform файл с нужными нам переменными нового окружения cat > main.tf << END module "base" { source = "../../modules/azure" env_name = "${ENV_NAME}" vnet_address = "${SUBNET}" subnet_address = "${SUBNET}" } END # Cоздаём backend.tf terraform файл , в котором указываем имя нового state файла cat > backend.tf << END terraform { backend "azurerm" { storage_account_name = "terraform-user" container_name = "environments" key = "${ENV_NAME}.tfstate" } } END } delete_env() { # удаляем каталог окружения и высвобождаем подсеть if [ -d "${DEPLOY_DIR}/${ENV_NAME}" ]; then NET=$(sed -e '$!d' ./${DEPLOY_DIR}/"${ENV_NAME}"/subnets.txt) echo "Release subnet: ${NET}" echo "$NET" >> ./"${SCRIPT_DIR}"/subnets.txt pushd ./${DEPLOY_DIR}/"${ENV_NAME}" || exit 1 popd || exit 1 rm -r ./${DEPLOY_DIR}/"${ENV_NAME}" else echo "Environment ${ENV_NAME} does not exist..." exit 1 fi } case "${ACTION}" in create) create_env git_push "${ACTION}" ;; delete) delete_env git_push "${ACTION}" ;; *) usage; exit 1;; esac 
  1. Сценарий env.shпринимает два параметра - имя окружения и действие(создание\удаление).

  2. При создании нового окружения:

  3. В каталоге DEPLOY_DIRсоздаётся директория с именем окружения.

  4. Из файла scripts/subnets.txt мы извлекаем одну свободную подсеть.

  5. На основе полученных данных генерируем конфигурационные файлы для Terraform.

  6. Для фиксации результата отправляем каталог с файлами окружения в git репозиторий.

  7. При удалении -сценарий удаляет каталог с файлами окружения и возвращает освободившуюся подсеть в файл scripts/subnets.txt

scripts/subnets.txt:

172.28.50.0172.28.51.0172.28.52.0...

В данном файле мы храним адреса наши подсетей. Размер подсети определяется переменной CIDR в файлеscripts/create_env.sh

Результат

  1. Мы получили фундамент который позволяет нам развернуть новый стенд путём запуска piplinea в Gitlab CI.

  2. Снизили затраты нашей компании на инфраструктуру.

  3. Разработчики не простаивают и могут создавать и удалять стенды когда им это потребуется.

  4. Также получили возможность создания виртуальных машин в любом облаке написав новый модуль Terraform и немного модифицировав сценарий созданиях\удаления окружений

  5. Можем поиграться с триггерами Gitlabи разворачивать новые стенды из пайплайнов команд разработчиков передавая версии сервисов.

Подробнее..

Почему клавиатура всегда быстрее мыши

27.05.2021 10:19:54 | Автор: admin

Тепловая карта с клавиатуры высокоинтеллектуальных программистов, источник: r/ProgrammerHumor/

Много говорилось об удовольствии, которое испытываешь при работе в консоли. Это не случайно: так задумано отцами-основателями Unix. Возникает странное ощущение, что это самый правильный способ взаимодействия с компьютером.

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

В чём же дело?

Экзотический манипулятор


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

Необычные манипуляторы с колёсиком стоили в районе 400 долларов. Затем вышел революционный компьютер Apple Lisa один из первых ПК с графическим интерфейсом. Компания Apple демпинговала она снизила стоимость манипулятора до 25 долларов и сделала сексуальный дизайн с одной кнопкой. Мышь из профессионального аксессуара превратилась в массовый гаджет.


Apple Lisa. Очень элегантный дизайн для своего времени

С тех пор мышь и GUI стали прочно ассоциироваться с компьютерами Apple и модным оконным интерфейсом.

Вскоре появилась Windows. Затем первые игры с поддержкой мыши, например, для более точного движения персонажа Возникает впечатление, что компьютерные игры постоянно стимулировали технический прогресс, в том числе производительность CPU, GPU, ёмкость накопителей, популярность мышей и других экзотических манипуляторов.

Геймеры вечно недовольны текущей производительностью персональных компьютеров и непрерывно требуют от индустрии наращивать мощность железа. Если посмотреть, кто собирает самые производительные конфигурации ПК и покупает самые мощные версии десктопных процессоров, то это будут сотни любителей рекордных FPS, парочка учёных с машинным обучением и один программист, который каждый день что-то билдит и компилирует. Да и то, трём последним персонажам компьютер нужен конкретно для работы, так что они не всегда вкладываются собственными деньгами, как геймеры.

Если бы не компьютерные игры, то производители процессоров могли сосредоточиться исключительно на серверных CPU. В самом деле, армия бухгалтеров, экономистов и прочих офисных клерков спокойно посидят на компьютерах 20-летней давности, которые их полностью устраивают. Они вообще не в курсе, какое железо стоит внутри процессора (так они называют системный блок). Зато не отрывают руку от любимой мышки. Отними у офисного клерка мышь и он будет несколько минут тупо пялиться в монитор и бессмысленно дёргать рукой, не в силах совершить ни одного полезного действия, словно под седативными веществами.

В наше время редко встретишь компьютер без мыши. А вот удовольствие от работы в консоли осталось.

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

Крутые однострочники


Вот некоторые примеры интересного использования программ Linux.

ps aux | convert label:@- process.png

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

Примечание. Утилита convert входит в пакет ImageMagick, так что нужно сначала его установить.

А вообще, текст из консоли можно быстро запостить через интернет-сервис вроде termbin.com (это как pastebin, только для консоли):

ps aux | nc termbin.com 9999

Как обычно, с алиасом для частого использования:

alias tb='nc termbin.com 9999'

Следующая:

curl ipinfo.io

Это если хотите узнать свой внешний IP-адрес через сервис ipinfo.io.

git log --format='%aN' | sort -u

Очень удобная команда, если работаете над опенсорсным проектом и хотите посмотреть контрибуторов.

history | awk '{print $2}' | sort | uniq -c | sort -rn | head

Отсортированный список самых часто запускаемых команд (тоже полезно добавить в алиасы, чтобы запускать в пару нажатий).

ls -d */

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

du -hs */ | sort -hr | head

Эта команда показывает только 10 крупнейших директорий в текущем каталоге.

ss -p

Просмотр, какие приложения потребляют трафик (утилиты iftop и nethogs дают более подробную информацию).

rm -f !(test.txt)

Команда удаляет из директории все файлы, кроме одного, указанного в скобках. Это работает после включения расширенной глобуляции в баше (shopt -s extglob).

python3 -m http.server

Запускает http-сервер и начинает отдавать файлы. Удобно, если хотите пошарить какой-то html-файл по сети.

screen -S the-screen-name

Создание экран-сессии.

screen -x the-screen-name

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

Утилита screen поставляется по умолчанию со многими дистрибутивами Linux, хотя не со всеми.

alias copy='xclip -i -selection clipboard'

cat file.txt | copy

Копирование файла в буфер обмена, когда первый однострочник прописан как алиас copy в баше.

sudo !!

Запустить последнюю команду под рутом, если в предыдущей команде вы забыли набрать sudo. У этой команды первое место в рейтинге однострочников.

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

Горячие клавиши как наследие консоли


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

Алиасы bash служат той же цели: выполнить команду с наименьшим количеством усилий, то есть с наименьшим количеством нажатий клавиш.

Взаимодействие с машиной у опытного специалиста происходит практически со световой скоростью. Скорость ограничивается только временем обработки команд на CPU и скоростью вывода текста в консоль, то есть скоростью графической подсистемы. А с сетевыми задержками компьютер работает медленнее, чем человек. Это идеал производительности человека.

Если подняться на более высокий уровень абстракций, то горячие клавиши в графических интерфейсах это в каком-то смысле дальний потомок консоли, некое историческое эхо. С горячими клавишами, однострочниками и алиасами в баше человек становится быстрее компьютера приходится делать паузы, чтобы дождаться результата выполнения команд. CPU загружен практически на 100%.

Это фундаментальное преимущество клавиатуры как инструмента для ввода команд по сравнению с любыми манипуляторами. В этом же и сила консоли.

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

В общем, из этого факта может родиться предположение, что отцы-основатели Unix всё-таки были правы, а их наследие живёт во всех операционных системах. Графическая оболочка просто тонкий слой абстракции поверх мощного фундамента, который они построили. Ведь мы помним, что macOS тоже основана на Unix и относится к семейству *nix-систем.

Ну а окошки и другие элементы графического интерфейса Windows, по мнению Apple, это вторичный продукт, скопированный с интерфейса Lisa (см. судебный процесс Apple против Microsoft c 1988 по 1994 гг).

Суд отклонил иск Apple к Microsoft. Но некоторые вещи обращают на себя внимание. Например, команда open . в консоли macOS открывает Finder в текущей директории. В Windows то же самое делает команда start . (Finder здесь называется Explorer). Окна в macOS закрываются крестиком в левом верхнем углу, а в Windows в правом углу. Возможно, на примере таких деталей Билл Гейтс доказал суду, что у него оригинальный графический интерфейс, который сильно отличается от macOS.

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



На правах рекламы


Наша компания предлагает аренду VPS для совершенно любых проектов. Создайте собственный тарифный план в пару кликов, максимальная конфигурация позволит разместить практически любой проект 128 ядер CPU, 512 ГБ RAM, 4000 ГБ NVMe!

Присоединяйтесь к нашему чату в Telegram.

Подробнее..

Консольный менеджер сертификатов для JKSPCKS12

05.02.2021 14:16:53 | Автор: admin

Привет Хабр!

С момента популяризации https, работа с сертификатами теперь есть практически в каждом проекте.

habr-certificatehabr-certificate

Сертификаты, которые любой может сгенерировать сам;
Сертификаты которые выдает местный СА в компании, доверие к которому есть только внутри компании;
Бесплатный публичный let's encrypt
Платный сертификат от крупных центров авторизации с дополнительными услугами, типа проверки компании.

Сертификаты для tls/ssl, сертификаты для авторизации, сертификаты для two-say-tls/ssl - даже в пределах одного небольшого проекта с десятком микросервисов и несколькими тестовыми окружениями - сертификатов набирается уйма.

При работе с множеством сертификатов, их удобно хранить не отдельными файлами, а складывать в хранилища (keystore). Самые популярные keystore форматы на сегодня - JKS (java keystore, который считается legacy с выходом 9-й джавы) и PKCS12. Последний вышел уже около 20 лет назад, и несмотря на некоторую критику, является одним из самых надежных, открытых и популярных форматов, который в скором будущем должен уже полностью вытеснить JKS.

Я много работал в проектах, где основным языком разработки был java, поэтому при работе с keystore основной инструмент для меня это консольная утилита keytool, которая поставляется с JDK. И для большинства популярных действий у меня конечно есть микро-шпаргалка:

Посмотреть список сертификатов:

keytool -list -v -keystore "файл.jks" -storepass "пароль"|grep -P '(Alias name:|Entry type:|Serial number:|Owner:|Valid from:)'

Скопировать сертификат из одного keystore в другой keystore:

keytool -importkeystore -srckeystore "откуда.jks" -srcstorepass "пароль" -destkeystore "куда.jks" -deststorepass "пароль" -srcalias "имя сертификата" [ -destalias "новое имя" ]

Импортировать сертификат в keystore из PEM файла:

keytool -import -file "откуда.pem" -keystore "куда.jks" -storepass "пароль" -noprompt -alias "имя_сертификата"

Экспортировать сертификат из keystore в файл:

keytool -exportcert -v -alias "alias" -keystore "keystore.jks" -storepass "storepassword" -rfc -file "exportedcert.cer"

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

Возможно, я бы собрался силой воли, помучился бы недельку, сделал бы все стандартными однострочниками+notepad+excel, но тут внезапно у меня случился больничный отпуск, а когда температура меня попустила, а на работу еще можно было не выходить, я случайно вспомнил, что считаю себя спецом по bash.

И пусть я не хватаю лавры автора PIUPIU), но тем не менее встречайте:

Консольный двух-панельный keystore менеджер на Linux shell.

Сперва я написал рабочий вариант на bash, но в какой-то момент подумал и переписал все башизмы в пользу POSIX совместимости. Код стал выглядеть более громоздко, зато скрипт проверенно работает в bash/ksh/dash без проблем.
Если я что-то пропустил - напишите в комментариях, ну или форкайте на здоровье ;)

Также используется sed и grep, по поводу последнего - я не очень уверен, насколько POSIX-совместимо grep -P. Но если что, там несложно переписать на sed.

Некоторое сожаление: привычка работы с java вынудила меня всю работу с хранилищами выполнять именно через keystore, который должен быть доступен в PATH (а может быть стоило разобраться и сделать все через openssl?).

Но давайте к делу. Что умеет менеджер вкратце можно наглядно увидеть на скриншотах.

В одно-панельном режиме:

В двух-панельном режиме:

Для поклонников панельных менеджеров (NC, VC, MC, FAR и др), функциональные клавиши и навигация должны быть интуитивно понятны.

Весьма важным плюсом я считаю, что jks_mgr.sh - это просто шелл скрипт, одним файлом размером 20 килобайт (update: уже 30+ кб) (update2: уже 35+ кб).

Никаких обсфукаций, код максимально простой - проверить на отсутствие закладок может в принципе любой джуниор - то есть такой скрипт можно использовать в любом проекте, не нарушая никаких требований по безопасности, требований по лицензионности, лоялен к производительности, установка не нужна - лишь бы на целевой машине был доступен шелл, keytool, sed и grep.

Что было самое интересное во время разработки:

Автоматическая подстройка панелей под высоту/ширину экрана.

Так как экран у меня перерисовывается почти с каждым нажатием, то менять ширину экрана (например если сидеть через графический ssh клиент) можно в произвольное время - после первого же нажатия любой клавиши экран перерисуется и все адаптируется. Пару вечеров ушло на опции отображения/скрытия столбцов - нужно было высчитать и отладить подстройку для одно-панельного и двух-панельного режима, плюс заголовки и текст считается разными формулами. Вот кусочек для определения насколько надо обрезать Certificate Alias, если экран слишком узкий и затем пример вывода заголовка:

WindowWidth="$(tput cols)"if [ -n "$RFILE" ]; then # two-panel    used=24    [ -n "$SHOW_TYPE" ] && used=$(( $used+34 ))    localWidth=$(( ( $WindowWidth - $used ) / 2 - 1 ))    if [ $localWidth -ne $aliasWidth ]; then        aliasWidth=$localWidth        [ $aliasWidth -lt 1 ] && aliasWidth=1        clear    fi..................................headerWidth=$(( $aliasWidth + 5 ))[ -n "$SHOW_TYPE" ] && headerWidth=$(( $headerWidth + 17 ))printf " store: ${blue}%-$(( $headerWidth ))s${rst}" "$LFILE"printf "| store: ${blue}%-$(( $headerWidth -1 ))s${rst}\n" "$RFILE"printf " %-10s" "Valid to"[ -n "$SHOW_TYPE" ] && printf " %-16s" "Storetype"printf " %-${aliasWidth}s |" "Alias"printf " %-10s" "Valid to"[ -n "$SHOW_TYPE" ] && printf " %-16s" "Storetype"printf " %-${aliasWidth}s\n" "Alias"

Определение нажатия функциональных клавиш.

Некоторые специальные keypress могут иметь длину до 4 символов, поэтому просто через read было сделать непросто - он не позволяет вариативно менять длину читаемой строки, но после поиска в гугле и экспериментов c read в bash/dash, это выглядит так:

# Special keypress could take up to 4 charactersread -rsN1 keypressif [ "$keypress" == "$escape_char" ]; then    read -sn1 -t 0.01 k1    read -sn1 -t 0.01 k2    read -sn1 -t 0.01 k3    read -sn1 -t 0.01 k4    keypress=${k1}${k2}${k3}${k4}    unset k1 k2 k3 k4fi

Теперь можно сравнивать $keypress с комбинациями типа '[A' (стрелка вверх), '[B' (стрелка вниз), '[13~' (F3) и так далее. Я не совсем уверен, что мой вариант будет работать везде идеально - поэтому на всякий случай почти все хоткеи продублированы обычными буквами.

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

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

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

В конце нужно написать какой-то умный итог и заключение ну он простой:

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

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

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

P.S. На фоне некоторых проблем с nginx, Wargaming и др., сразу хочу уточнить, что jks_mgr.sh был написан ИСКЛЮЧИТЕЛЬНО в нерабочее время на личном компьютере.

Подробнее..

Recovery mode DevOps автоматизация инфраструктуры на примере Terraform, docker, bash, prometheus exporters, Gitlab и WireGuard

16.03.2021 12:09:05 | Автор: admin

Всем привет.

Есть такие люди, которые работают с облачной инфраструктурой и не используют автоматизацию, потому что это долго, нужно вникать, а им надо фичи пилить. Накликали что-то там в UI, подключились по ssh, поставили всякого с помощью apt и т.д. и конфигурационные файлы ещё вручную поменяли. Документации конечно же написать времени не хватило или в ней много разных хитрых шагов и повторить настройку этой инфраструктуры в точности уже нельзя или очень сложно, а сервисы крутятся в проде. А потом человек забыл что и как делал в точности или вообще уволился.

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

К слову, крайне рекомендую для автоматизации много чего, но в особенности облачных провайдеров вроде AWS / GCP / Azure и т.д. использовать именно Terraform, т.к. это достаточно зрелый инструмент, у него большое сообщество и кроме всего прочего он поддерживает автоматизацию далеко не только каких-то облачных провайдеров, но и практически всего у чего есть API. К тому же инструмент open source и при желании можно реализовать что угодно самостоятельно. Для таких облаков, как AWS не рекомендую пытаться реализовывать автоматизации с помощью чистого питона и запросов к AWS API с помощью cli или Cloudformation.

Также у Terraform есть удобная возможность организовывать код в блоки называемые модулями и передавая в них только параметры легко создавать необходимое с другими настройками.

Для тех, кто совсем не знаком с Terraform упомяну, что если в одну папку положить несколько файлов с расширением .tf и запустить Terraform в этой папке, то Terraform прочитает и использует код из всех файлов, а не только одного. Это в том числе позволяет разбивать один большой main.tf файл с кучей ресурсов на какие-то удобные вам логические блоки.

Итак, например, встала задача развернуть vpn серверы WireGuard на базе Ubuntu 20.04 в нескольких регионах + немного мониторинга. Поддержка WireGuard сейчас есть в ядре linux, но дополнительные инструменты, которые можно поставить отдельно облегчают жизнь, поэтому поставим и их.

Весь код модуля выложен здесь.

Также этот модуль опубликован в реестре модулей здесь.

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

Итак, создаются необходимые для работы iam политики, роль и т.п.

Используем elastic ip, отдельный для сервера в каждом регионе, которые нужно прописать в dns для того, чтобы пользователь мог использовать единое имя для подключения к vpn серверу. Планировал использовать geo dns route53, чтобы при местоположении пользователя в оперделённом регионе ему бы отдавался ip vpn сервера в его регионе, но т.к. на этом проекте route53 пока не используется, то создание записей в нём пока не автоматизировал.

Создаются security groups с правилами, которые позволяют подключиться к vpn серверу извне по udp (Wireguard работает только по udp) + ssh + несколько портов для prometheus exporter'ов.

Создаётся собственно сервер / ec2 машина, но не просто отдельно стоящая, а входящая в auto scaling group, в данном примере в единственном варианте. Это сделано для того, чтобы если с сервером что-то не так, то Амазон автоматом пересоздаст его. Self healing.

Позже немного допилив конфигурацию и добавив в неё load balancer можно добиться того, для чего auto scaling groups отлично подходят: при повышенной нагрузке на какой-то из ресурсов сервера, например на cpu, можно реализовать автоматическое создание дополнительных vpn серверов, а соответственно при падении нагрузки уменьшать их количество.

Этот модуль можно использовать просто с Terraform, но лучше использовать Terragrunt, который позволяет делать некоторые удобные вещи и местами реализовывать концепцию Keep your Terraform code DRY, например параметризуя некоторые вещи в backend блоке, чего сам Terraform пока не умеет. Terraform хранит состояние инфраструктуры в специальном файле и принято хранить его не локально, а, чаще всего, в S3 бакете. Также, если вы работаете с этим кодом не в одиночку, то принято хранить локи в Dynamodb, чтобы случайно не применить какое-то изменение инфраструктуры несогласованно и не поломать всё.

Именно пример такого использования я привожу в примере здесь.

С помощью файла terragrunt.hcl в корне репозитория (https://github.com/vainkop/terraform-aws-wireguard/blob/master/example/terragrunt.hcl) я могу, например, задать место для хранения state для всех поддиректорий, а потом ссылаться на этот файл в других terragrunt.hcl с помощью функции find_in_parent_folders() https://github.com/vainkop/terraform-aws-wireguard/blob/master/example/us-east-1/terragrunt.hcl#L2

При этом key, т.е. файл, где будет храниться состояние инфраструктуры в конкретном регионе будет храниться отдельно, что достигается с помощью функции path_relative_to_include() https://github.com/vainkop/terraform-aws-wireguard/blob/master/example/terragrunt.hcl#L11

Также я реализовал хранение/чтение переменных в yaml формате, что мне кажется более удобочитаемым с помощью функции yamldecode(file(...)) https://github.com/vainkop/terraform-aws-wireguard/blob/master/example/eu-central-1/terragrunt.hcl#L9

Вот так выглядит пример передаваемых в модуль уникальных параметров (конечно YOUR_... нужно заменить на реальные значения): https://github.com/vainkop/terraform-aws-wireguard/blob/master/example/us-east-1/values.yaml

Иногда удобно реализовать использование имени папки в качестве параметра, например в приведённом примере это мог бы быть параметр region и реализуется это с помощью, например, функций basename(get_terragrunt_dir()) и задавать его в values.yaml не пришлось бы, но по определённым причинам решил этого не делать.

В итоге в вашем приватном репозитории код из которого применяете либо вы, либо какой-то ci cd runner может лежать только содержимое похожее на мою папку example, т.е. только terragrunt.hcl и yaml файлы с параметрами, а модуль можно использовать как публичный и хорошо поддерживаемый, так и написать свой. Это позволяет отдать "пользователям" только задание параметров в yaml и в принципе ничего не знать про Terraform код.

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

Для того, чтобы изменения в коде open source модулей, как впрочем и в частных, не повлияли на работу вашей автоматизации принято фиксировать версии используемых модулей, например в моём коде это сделано с помощью source = "github.com/vainkop/terraform-aws-wireguard?ref=v1.2.0" здесь https://github.com/vainkop/terraform-aws-wireguard/blob/master/example/eu-central-1/terragrunt.hcl#L6

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

Например я реализовал cloud-init скрипт, который осуществляет предварительную установку и настройку софта на свежеразвёрнутый сервер и делает это каждый раз, когда сервер пересоздаётся в auto scaling group, что очень удобно: https://github.com/vainkop/terraform-aws-wireguard/blob/master/templates/user-data.txt

Ближе к концу скрипта устанавливается 2 prometheus exporter'а, которые позволяют как мониторить метрики самой ec2 машины, так и базовые метрики самого WireGuard, на основании которых можно построить удобные Dashboards и соответственно определённые alerts и т.п.

В частности я реализовал это для того, чтобы видеть к какому из vpn серверов подключён клиент, чтобы, например, была возможность подключиться к нему именно из его региона, т.к. связности между этими vpn серверами нет. Т.к. клиентские публичные ключи зашиты в каждый из серверов и серверные ключи одинаковые, то клиент будет автоматически переключаться между ними путешествуя между регионами на основании geo ip route53.

Также привожу пример кода из .gitlab-ci.yml и Dockerfile где можно увидеть какие команды используются для применения всего этого хозяйства с помощью Gitlab runner'а и какой docker контейнер можно использовать для этого runner'а.

$ cat .gitlab-ci.ymlstages:  - build  - plan  - apply  - destroyvariables:  GIT_DEPTH: 1.aws_configure: &aws_configure  before_script:    - aws configure set aws_access_key_id $AWS_ACCESS_KEY_ID    - aws configure set aws_secret_access_key $AWS_SECRET_ACCESS_KEY    - aws configure set default.region $AWS_DEFAULT_REGIONbuild-terraform:  image: docker:19.03.15  services:    - docker:19.03.15-dind  stage: build  variables:    DOCKER_TLS_CERTDIR: ""    DOCKER_HOST: tcp://docker:2375    DOCKER_DRIVER: overlay2    TERRAFORM_VERSION: "0.13.6"    TERRAGRUNT_VERSION: "v0.28.9"  before_script:    - printenv    - docker info    - echo $CI_REGISTRY_PASSWORD | docker login $CI_REGISTRY -u $CI_REGISTRY_USER --password-stdin  script:    - cd docker    - docker build --build-arg TERRAFORM_VERSION=$TERRAFORM_VERSION --build-arg TERRAGRUNT_VERSION=$TERRAGRUNT_VERSION -t $CI_REGISTRY_IMAGE:$TERRAFORM_VERSION .    - docker push $CI_REGISTRY_IMAGE:$TERRAFORM_VERSION  rules:    - changes:        - docker/*plan-us-east-1:  image:    name: registry.gitlab.com/vainkop/terraform:0.13.6    entrypoint: [""]  stage: plan  <<: *aws_configure  script:    - cd wireguard/us-east-1    - terragrunt run-all plan --terragrunt-non-interactive -out $CI_PROJECT_DIR/wireguard/us-east-1/tfplan-$CI_COMMIT_SHA  artifacts:    paths:    - $CI_PROJECT_DIR/wireguard/us-east-1/tfplan-$CI_COMMIT_SHA    expire_in: 1 month  rules:    - changes:        - wireguard/us-east-1/*      allow_failure: trueplan-eu-central-1:  image:    name: registry.gitlab.com/vainkop/terraform:0.13.6    entrypoint: [""]  stage: plan  <<: *aws_configure  script:    - cd wireguard/eu-central-1    - terragrunt run-all plan --terragrunt-non-interactive -out $CI_PROJECT_DIR/wireguard/eu-central-1/tfplan-$CI_COMMIT_SHA  artifacts:    paths:    - $CI_PROJECT_DIR/wireguard/eu-central-1/tfplan-$CI_COMMIT_SHA    expire_in: 1 month  rules:    - changes:        - wireguard/eu-central-1/*      allow_failure: trueapply-us-east-1:  image:    name: registry.gitlab.com/vainkop/terraform:0.13.6    entrypoint: [""]  stage: apply  <<: *aws_configure  script:    - cd wireguard/us-east-1    - terragrunt run-all apply --terragrunt-non-interactive -auto-approve $CI_PROJECT_DIR/wireguard/us-east-1/tfplan-$CI_COMMIT_SHA  rules:    - changes:        - wireguard/us-east-1/*      when: manual      allow_failure: trueapply-eu-central-1:  image:    name: registry.gitlab.com/vainkop/terraform:0.13.6    entrypoint: [""]  stage: apply  <<: *aws_configure  script:    - cd wireguard/eu-central-1    - terragrunt run-all apply --terragrunt-non-interactive -auto-approve $CI_PROJECT_DIR/wireguard/eu-central-1/tfplan-$CI_COMMIT_SHA  rules:    - changes:        - wireguard/eu-central-1/*      when: manual      allow_failure: truedestroy-us-east-1:  image:    name: registry.gitlab.com/vainkop/terraform:0.13.6    entrypoint: [""]  stage: destroy  <<: *aws_configure  script:    - cd wireguard/us-east-1    - terragrunt run-all destroy --terragrunt-non-interactive -auto-approve  rules:    - changes:        - wireguard/us-east-1/*      when: manual      allow_failure: truedestroy-eu-central-1:  image:    name: registry.gitlab.com/vainkop/terraform:0.13.6    entrypoint: [""]  stage: destroy  <<: *aws_configure  script:    - cd wireguard/eu-central-1    - terragrunt run-all destroy --terragrunt-non-interactive -auto-approve  rules:    - changes:        - wireguard/eu-central-1/*      when: manual      allow_failure: true
$ cat docker/DockerfileFROM ubuntu:20.04USER rootARG DEBIAN_FRONTEND=noninteractiveARG TERRAFORM_VERSIONENV TERRAFORM_VERSION=$TERRAFORM_VERSIONARG TERRAGRUNT_VERSIONENV TERRAGRUNT_VERSION=$TERRAGRUNT_VERSIONRUN set -x && \    apt-get update && \    apt-get install -y \    apt-transport-https \    ca-certificates \    build-essential \    software-properties-common \    unzip \    net-tools \    wget \    curl \    python3 \    python3-dev \    python3-pip \    jq \    gettext-base \    git && \    rm -rf /var/lib/apt/lists/*RUN set -x && \    apt-key adv --keyserver keyserver.ubuntu.com --recv-keys CC86BB64 && \    add-apt-repository ppa:rmescandon/yq && \    apt update && \    apt install -y yq && \    rm -rf /var/lib/apt/lists/*RUN set -x && \    pip3 install -U --no-cache-dir setuptools shyamlRUN set -x && \    ln -sf /usr/bin/python3 /usr/bin/python && ln -sf /usr/bin/pip3 /usr/bin/pipRUN set -x && \    curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" && \    unzip awscliv2.zip && \    rm awscliv2.zip && \    ./aws/installRUN set -x && \    cd /tmp && \    curl -O https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip && \    unzip terraform_${TERRAFORM_VERSION}_linux_amd64.zip -d /usr/local/bin && \    chmod +x /usr/local/bin/terraform && \    rm /tmp/terraform_${TERRAFORM_VERSION}_linux_amd64.zipRUN set -x && \    wget "https://github.com/gruntwork-io/terragrunt/releases/download/${TERRAGRUNT_VERSION}/terragrunt_linux_amd64" && \    mv terragrunt_linux_amd64 /usr/local/bin/terragrunt && \    chmod +x /usr/local/bin/terragruntRUN set -x && \    curl --version && \    envsubst --version && \    python --version && \    pip --version && \    shyaml --version && \    jq -V && \    yq -V && \    aws --version && \    terraform --version && \    terragrunt --versionENTRYPOINT ["/bin/bash", "-c"]

За код не ругайте, написал за несколько часов и решил поделиться.

Если есть конкретные замечания/предложения, то готов их выслушать либо в комментариях, либо в личке, например, в телеграм: @vainkop

Прошу также учитывать, что это моя первая публикация на Хабре. Приглашение приму с удовольствием.

Подробнее..

GitOps Определение дрейфа вашей инфраструктуры Terraform Terragrunt

18.03.2021 14:16:42 | Автор: admin

Всем привет.

Дисклеймер: сказу скажу, что пишу статью по-ходу дела, "код" в ней рабочий, но не претендует на какие-либо best practices, поэтому не придирайтесь :) Цель статьи: донести до интересующейся русскоязычной части населения общие принципы, возможно разбудить интерес поразбираться самостоятельно и сделать что-то гораздо лучше и интереснее. Итак поехали!

Допустим Вы работаете с Terraform / Terragrunt (второе здесь непринципиально, но лучше изучайте, если ещё не используете) и автоматизируете инфраструктуру, например, в AWS (но совершенно необязательно AWS). Инфраструктура в коде репозитория, разворачивается из него же, казалось бы вот оно GitOps счастье :)

Всё идёт хорошо, пока какой-то пользователь не поменял что-то руками через консоль / UI и конечно забыл об этом кому-либо сказать. А то и сделал что-то нехорошее намеренно. И вот он ваш дрейф: код и инфраструктура больше не совпадают! :(

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

Как обычно, есть много различных путей добиться желаемого. Например, недавно на горизонте появилась неплохо развивающаяся утилита https://github.com/cloudskiff/driftctl , которая может даже больше, чем предложу Вашему вниманию чуть ниже я, но на момент написания статьи driftctl как минимум не поддерживает работу с aws provider v2, а также не умеет в multi region, что делает его использование невозможным в большинстве серьёзных проектов. Но ребята обещают доделать её через месяц-два.

А пока что опишу и приведу пример небольшого количества кода для следующей очень простой схемы:

1) создаём pipeline, который или по расписанию (в Gitlab можно воспользоваться Pipeline schedules) или по кругу будет делать terraform plan

2) при нахождении дрейфа (diff в плане) pipeline будет, например, отправлять сообщение с его содержанием в Slack.

Аналогично можно реализовать и, например, создание issue в любом из используемых вами репозиториев, где поддерживается их создание через api и любое другое действие, например apply, который вернёт инфраструктуру к её эталонному состоянию. Или всё-таки импортировать изменение в state, если оно действительно необходимо.

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

account_1/ eu-central-1  dev   eks    terragrunt.hcl    values.yaml   s3-bucket       terragrunt.hcl       values.yaml  prod   eks    terragrunt.hcl    values.yaml   s3-bucket       terragrunt.hcl       values.yaml  staging      eks       terragrunt.hcl       values.yaml      s3-bucket          terragrunt.hcl          values.yaml us-east-1  dev   eks    terragrunt.hcl    values.yaml   s3-bucket       terragrunt.hcl       values.yaml  prod   eks    terragrunt.hcl    values.yaml   s3-bucket       terragrunt.hcl       values.yaml  staging      eks       terragrunt.hcl       values.yaml      s3-bucket          terragrunt.hcl          values.yaml terragrunt.hcl

В приведённом выше примере в папке account_1 находятся 2 папки: us-east-1 и eu-central-1 , по имени регионов AWS. Иногда удобно организовать структуру именно так и тогда имена папок можно использовать как значение для передачи в модуль с помощью Terragrunt функции/й, например, таких "${basename(get_terragrunt_dir())}"

Аналогичная логика с папками имеющими в названии окружение и далее идут названия самих компонентов, которых в этом примере 2: eks и s3-bucket

Если смотреть от корня репозитория, то путь до каждого из файлов внутри папки компонента

<account_name>/<region>/<environment>/<component>/*

Т.е. "в общих чертах" */*/*/<component>/*

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

Не забудьте подключить Incoming WebHooks в Slack и записать полученный Webhook URL. Делается это так: https://api.slack.com/messaging/webhooks

Тогда вот такой скрипт может выполнять требуемое планирование в pipeline и отправку в Slack diff'а при его нахождении:

#!/bin/bashROOT_DIR=$(pwd)plan () {  echo -e "$(date +'%H-%M-%S %d-%m-%Y') $F"  CURRENT_DIR=$(pwd)  PLAN=$CURRENT_DIR/plan.tfplan  terragrunt run-all plan --terragrunt-non-interactive -lock=false -detailed-exitcode -out=$PLAN 2>/dev/null || ec=$?    case $ec in    0) echo "No Changes Found"; exit 0;;    1) printf '%s\n' "Command exited with non-zero"; exit 1;;    2) echo "Changes Found! Reporting!";          MESSAGE=$(terragrunt show -no-color ${PLAN} | sed "s/\"/'/g");    # let's replace the double quotes from the diff with single as double quotes "break" the payload       curl -X POST --data-urlencode "payload={\"channel\": \"#your-slack-channel-here\", \"username\": \"webhookbot\", \"text\": \"DRIFT DETECTED!!!\n ${MESSAGE}\", \"icon_emoji\": \":ghost:\"}" https://hooks.slack.com/services/YOUR/WEBHOOK/URL_HERE;;  esac}N="$(($(grep -c ^processor /proc/cpuinfo)*4))"    # any number suitable for your situation goes herefor F in */*/*/s3-bucket/*; do  ((i=i%N)); ((i++==0)) && wait    # let's run only N jobs in parallel to speed up the process  cd $ROOT_DIR  cd $F  plan &    # send the job to background to start the new onedone

Меняем что-нибудь руками, запускаем pipeline или ждём его выполнения и радуемся :)

На этом на сегодня всё!

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

P.S. имхо проект https://github.com/cloudskiff/driftctl мне лично кажется действительно полезным и решающим правильную задачу и хороших аналогов ему нет, так что прошу поддержать ребят, а по-возможности внести свою лепту ибо open source.

Всем хорошего настроения!

Подробнее..

Полезные материалы для разработчика

19.03.2021 12:22:16 | Автор: admin

Существует много полезных инструментов, которые помогают автоматизировать рутинную работу и тем самым облегчить жизнь разработчика.

Выпускнику и преподавателю Computer Science Center, Равилю Галееву, пришла идея собрать такие инструменты и технологии в один курс и познакомить студентов с ними. За пример такого курса были взяты The Missing Semester of Your CS Education от MIT, Software Carpentry и cs50.

В этом посте мы собрали видеолекции курса Практический минимум и материалы к занятиям. Благодарим Равиля за подборку!

Содержание

Введение в Linux

Командная строка Linux

Система контроля версий git

Языки разметки и XML

Регулярные выражения

Взаимодействие с сетью

Протокол HTTP

Контейнеризация

Архитектура приложений

Тестирование приложений

Опасность в приложениях

Билд-системы

Кодировки, даты, локали

Дебаг

Набор в Computer Science Center 2021

Введение в Linux

  • Буквально пара слов о том, что такое ядро

  • Набор исторических фактов (от Unix к Linux)

  • Файловая система

  • Пользователи

  • Файлы

  • Процессы

  • Unix way

Слайды

Статьи

Wikipedia History of Unix

Книги

Видео

Курсы

Командная строка Linux

  • bash как REPL

  • Unix way

  • Шебанг

  • make

Слайды

Статьи

Книги

Ian Miell Learn Bash the Hard Way

Видео

Слайды/Презентации

Bash-скрипты из реального мира

Система контроля версий git

  • git

    • commit

    • branch

    • merge

  • git flow

  • github

Слайды

Статьи

Книги

  • Scott Chacon and Ben Straub Pro Git

Видео

Потренироваться

Языки разметки и XML

  • groff

  • LaTex

  • XML, JSON, YAML

  • Markdown, AsciiDoc

  • GraphViz, PlantUML

Слайды

Статьи

Книги

К. В. Воронцов LATEX в примерах

Видео

Слайды и другие материалы

Markdown cheatsheets

Разное

Регулярные выражения

  • Регулярки

  • grep

  • sed

  • awk

Слайды

Статьи

Видео

Слайды и другие материалы

Взаимодействие с сетью

  • Разбираемся как работает посылка пакетов

  • Рассматриваем простейшие утилиты работы с сетью

  • Знакомимся с DNS, CDN, VPN и другими словами на три буквы

  • Пишем сервер на сокетах

Слайды

Материалы

Протокол HTTP

  • HTTP

  • REST

Слайды

Статьи

Видео

Разное

Контейнеризация

  • chroot

  • Docker

  • Docker compose

Слайды

Статьи

Видео

Курсы

Разное

Архитектура приложений

  • ООП

  • Паттерны

  • Многослойная архитектура

Слайды

Статьи

Книги

Курсы

Видео

Тестирование приложений

  • Тестирование

  • Логгирование

Слайды 1

Слайды 2

Статьи

Видео

Опасность в приложениях

  • Хеширование, контрольные суммы

  • Авторизация vs Аутентификация; JWT

  • Обмен ключами Диффи-Хеллман

  • RSA

  • TLS

  • Двухфакторная аутентификация

Слайды

Статьи

Видео

Книги

Билд-системы

  • от make к TravisCI

  • dockerhub

Слайды

Статьи

Видео

Разное

Anatomy of a Continuous Integration and Delivery (CICD) Pipeline

Кодировки, даты, локали

Разбираемся, почему /dev/random печатает краказябры

Слайды

Статьи

Видео

Дебаг

  • Исключения

  • Дебаг

Слайды

Статьи

Книги

Видео

Курсы

Кирилл Кринкин Основы программирования для Linux

Разное


Делитесь в комментариях своими рекомендациями материалов, которые пригодились вам.

Набор в Computer Science Center 2021

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

CS центр это вечерние курсы по математике и программированию. Занятия проходят в Санкт-Петербурге и в Новосибирске. Жители других городов могут поступить на обучение в удалённом формате.

Чтобы поступить:

заполните анкету на сайте до 10 апреля,

решите задания онлайн-теста до 11 апреля,

участвуйте в онлайн-экзамене в конце апреля-начале мая,

пройдите собеседование в мае-июне.

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

Задать вопросы про набор можно в телеграм канале или по почте info@compscicenter.ru.

Подробнее..

Перевод Оптимизация рабочего процесса при помощи fzf

05.04.2021 12:20:40 | Автор: admin

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


Обычные параметры моего рабочего процесса имена файлов и ветвей git: если посмотреть на мою историю команд, окажется, что git я ввожу чаще всего; ручной ввод команд git сопряжен с трудностями и часто приводит к ошибкам, поэтому я не ввожу команды руками везде, где это возможно. В зависимости от команды может подойти автозамена по табуляции, и она может оказаться очень полезной, но удобна она не всегда. В этом посте я покажу, как в качестве альтернативы использовать fzf.

Базовая функциональность fzf очень проста: он читает строки из стандартного потока ввода и даёт пользователю интерфейс, чтобы можно было выбрать одну или несколько строк и вписать их в стандартный поток вывода. Вики-страницы этого инструмента содержат массу примеров эффективного применения fzf. Это прекрасный ресурс, я взял несколько функций оттуда в свой репертуар и пользуюсь этими функциями почти каждый день, но в своей работе вы можете обнаружить очень специфические процессы, которых нет в вики, тогда как автоматизировать их было бы полезно.

Чтобы показать, как я подхожу к процессам подобного рода, я расскажу о 4 задачах, с которыми обычно сталкиваюсь. Затем напишу функцию оболочки с fzf, которая сделает работу удобнее. Кроме того, я расположу эти функции в порядке возрастания сложности:

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

Последние версии функций, включая варианты для fish, вы найдёте на Github.

Активация виртуальных сред python

Переменные моих виртуальных сред python содержится в файле ~/.venv. Вот, что я обычно делаю, чтобы активировать одну из сред:

  • начинаю ввод source ~/.venv/;

  • чтобы запустить автозавершение, нажимаю <tab>;

  • выбираю среду по желанию;

  • добавляю bin/activate и нажимаю <enter>.

Процесс можно улучшить чем-то вроде virtualenvwrapper, но есть и хороший пример с fzf: это простейшее решение, которое может занять всего одну строку.

function activate-venv() {  source "$HOME/.venv/$(ls ~/.venv/ | fzf)/bin/activate"}

activate-venv-simple.bash(download)

Активировать эту функцию можно с помощью команды:

source activate-venv-simple.bash

(добавьте этот код в свой .bashrc, чтобы он выполнялся постоянно), а затем используйте его, как показано ниже.

В окне выбора fzf показывает несколько виртуальных сред; среда активируется, когда строка выбрана.

Меньшая проблема то, что, если выйти из fzf нажатием ctrl-d, скрипт упадет с такой ошибкой:

bash: /home/crepels/.venv//bin/activate: No such file or directory

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

function activate-venv() {  local selected_env  selected_env=$(ls ~/.venv/ | fzf)  if [ -n "$selected_env" ]; then    source "$HOME/.venv/$selected_env/bin/activate"  fi}

Удаление веток git

Другим обнаруженным паттернам самоповторения оказалось удаление веток git. Как только фича смержена в основную ветку или эксперимент больше не нужен, ветка может быть удалена, но обычно я не удаляю её сразу: вместо этого ветки накапливаются, пока их количество не достигает критической точки, в которой навигация по веткам затрудняется. Когда дело в самом деле доходит до удаления веток, картина выглядит так:

  • я начинаю ввод git branch -D;

  • нажимаю табуляцию, чтобы вызвать автозавершение;

  • выбираю ветку, которую, как мне кажется, можно удалить.

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

Этот рабочий процесс хороший пример того, как сильно fzf упрощает работу. В этот раз мы воспользуемся опцией fzf --multi, которая позволяет по нажатию табуляции выбрать несколько записей.

function delete-branches() {  local branches_to_delete  branches_to_delete=$(git branch | fzf --multi)  if [ -n "$branches_to_delete" ]; then     git branch --delete --force $branches_to_delete  fi}

После выполнения source delete-branches-simple.bash мы можем использовать этот код следующим образом.

 Удаление веток при помощи fzf Удаление веток при помощи fzf

Код в основном работает, но реализовать эту функциональность можно по-разному. Первый вариант git branch показывает все ветки, включая ту, в которой мы находимся, она отмечена звёздочкой (*). Поскольку нельзя удалить ветку, в которой мы находимся, то и показывать её смысла нет, так что мы можем опустить эту ветку, предоставив вывод git branch команде grep --invert-match

Ещё один способ: мы можем пропустить переменным $branches_to_delete без кавычек в git branch -D. Сделать это нужно потому, что git каждая ветка нужна как отдельный аргумент. Если вы пользуетесь линтером вроде shallcheck, эта строка ему не понравится, поскольку переменные без кавычек могут вызвать глоббинг и разделение слов. В нашем случае срабатывание будет ложным: ветка не может содержать символов глоббинга; тем не менее я думаю, что избегать переменных без кавычек, где это возможно, хорошая практика, и один из способов сделать это пропустить вывод fzf через xargs прямо в git branch -D, а не хранить этот вывод в переменной. Если в xargs добавить опцию --no-run-if-empty, git будет вызываться только в том случае, если была выбрана хотя бы одна ветка.

Наконец, я упоминал, что, чтобы увидеть выбранную ветку, полезно посмотреть на вывод git log. Сделать это можно при помощи опции --preview: значением этой опции может быть какая-нибудь команда, которая будет выполняться всякий раз, когда в fzf будет выбрана новая строка, и вывод будет показан в окне предварительного просмотра. Фигурные скобки в этой команде работают как плейсхолдер, то есть заменяются на текущую выбранную строку.

function delete-branches() {  git branch |    grep --invert-match '\*' |    cut -c 3- |    fzf --multi --preview="git log {} --" |    xargs --no-run-if-empty git branch --delete --force}

Также обратите внимание на то, что вывод git branch пропускается через cut -с -3, которая из каждой строки удаляет 2 пробела. Если посмотреть на вывод git branch, видно, что каждая ветка, за исключением текущей, имеет префикс в 2 пробела. Если их не удалить, команда в --preview будет такой: git log ' branch-name', что приведёт к жалобам git на лишние начальные пробелы. В качестве альтернативы используйте команду git log {..}, которая тоже удалит пробелы из выбранной строки.

Вот пример: мы удаляем те же три ветки, что и выше, но при этом получаем больше информации.

Поток fzf для удаления ветвей в окне предварительного просмотра. Показаны ветки и вывод git log. Ударение ветвей с помощью fzf улучшенная версия.

Локально заходим в пул-реквест

Когда делается код-ревью, полезно бывает переключиться в ветку кода, который вы просматриваете . Интерфейс командной строки от гитхаба упрощает эту задачу: можно просто выполнить в репозитории команду пр pr-checkout. Так вы окажетесь в ветке соответствующего пул-реквеста и уже локально. Но как узнать номер пул-реквеста? Вот что я обычно делал:

  • открывал пул-реквест в браузере;

  • читал номер в URL;

  • переключался на окно терминала и вводил gh pr checkout, а затем номер.

Этот подход работает, когда мы имеем дело с пул-реквестом в 1 или 2 цифры, но, даже когда цифры всего 3, иногда я переключаюсь на браузер, чтобы убедиться, что запомнил номер правильно.

В моём прошлом посте я уже рассказывал, как при помощи gh автоматически опрашивал api Github, чтобы узнать номер пул-реквеста. Вы можете воспользоваться запросом к api, который я показываю ниже:

gh api 'repos/:owner/:repo/pulls'

Этот запрос возвращает массив JSON-объектов по одному объекту на каждый пул-реквест. Нам нужно конвертировать этот массив в подходящий fzf формат по строке на пул-реквест. Если говорить о данных, которые нам нужны, первое это номер пул-реквеста, который мы хотим пропустить через gh checkout. Также нам нужен способ идентифицировать интересный нам пул-реквест, в этом смысле лучший кандидат его заголовок. Чтобы извлечь эту информацию из JSON, мы можем воспользоваться интерполяцией строки в jq.

gh api 'repos/:owner/:repo/pulls' |    jq --raw-output '.[] | "#\(.number) - \(.title)"'

Вот опция сырого вывода --raw-output, которая определяет строку JSON; без неё каждая строка данных будет окружена кавычками. К примеру, если я выполню команду pr checkout https://github.com/junegunn/fzf, она выведет эти строки:

#2368 - ansi: speed up parsing by roughly 7.5x#2349 - Vim plugin fix for Cygwin 3.1.7 and above#2348 - [completion] Default behaviour to use fd if present else use find.#2302 - Leading double-quote for exact match + case sensitive search#2197 - Action accept-1 to accept a single match#2183 - Fix quality issues#2172 - Draft: Introduce --print-selected-count#2131 - #2130 allow sudo -E env fzf completion#2112 - Add arglist support to fzf.vim#2107 - Add instructions on command for installing fzf with Guix and/or Guix System#2077 - Use fzf-redraw-prompt in history widget#2004 - Milis Linux support#1964 - Use tmux shell-command#1900 - Prompt generally signals that the shell is ready#1867 - add {r}aw flag to disable quoting in templates#1802 - [zsh completion] Expand aliases recursively#1705 - Option to select line index of input feed and to output cursor line index#1667 - $(...) calls should be quoted: \"$(...)\"#1664 - Add information about installing using Vundle#1616 - Use the vim-specific shell instead of the environment variable#1581 - add pre / post completion 'hooks'#1439 - Suppress the zsh autocomplete line number output#1299 - zsh completion: Add support for per-command completion triggers.#1245 - Respect switchbuf option#1177 - [zsh] let key bindings be customized through zstyle#1154 - Improve kill completion.#1115 - _fzf_complete_ssh: support Include in ssh configs#559 - [vim] use a window-local variable to find the previous window#489 - Bash: Key bindings fixes

Пропустив их через fzf, мы сможем выбрать какие-то из этих строк и записать их стандартный поток вывода. Нам интересен только номер запятой, так что извлечём его при помощи команды set регулярного выражения с захватом группы. 1-я работающая версия выглядит так:

function pr-checkout() {  local pr_number  pr_number=$(    gh api 'repos/:owner/:repo/pulls' |    jq --raw-output '.[] | "#\(.number) \(.title)"' |    fzf |    sed 's/^#\([0-9]\+\).*/\1/'  )  if [ -n "$pr_number" ]; then    gh pr checkout "$pr_number"  fi}

Попробуем его на репозитории fzf.

Поток fzf в окне выбора показывает заголовки пул-реквестов. Выбирая строку, мы попадаем на соответствующий пул-реквест.

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

В этой функции мы удаляем ветки выше и заполняем окно предварительного просмотра с помощью вызова git log в выбранной ветви. Первой идеей может быть попытка попробовать то, что я уже показывал, то есть сделать запрос к api, чтобы получить информацию о выбранном реквесте. Но если мы выбираем разные ветви, то задержка запроса к api может начать нас раздражать, затрудняя работу. К счастью, запросы api нам больше не понадобятся: все нужные данные у нас уже есть: мы получили их, когда сделали первый запрос. Что нам нужно это дописать шаблон строки jq, чтобы извлечь всю нужную информацию и затем воспользоваться функцией fzf, которая позволяет спрятать информацию входящих строк в окне выбора и показать её в окне предпросмотра.

fzf рассматривает каждую строку как массив полей. По умолчанию поля разделяются последовательностями пробелов (табуляциями и пробелами), но мы можем управлять разделителем с помощью опции --delimiter. Например, если мы зададим --delimiter=',' и передадим строку first,second,third в fzf, то поля будут first,, second и third. Само по себе это бесполезно. Но с помощью опции --with-nth мы можем управлять полями в окне выбора. Например, fzf --with-nth=1,2 будет отображать только первое и второе поля каждой строки. Кроме того, мы видели выше, что можно написать {} в качестве плейсхолдера в команде предварительного просмотра и fzf заменит его текущей выбранной строкой. Но {} это простейшая форма плейсхолдера. Можно указать индексы полей в фигурных скобках, и fzf заменит плейсхолдер этими полями.

Вот пример, где мы используем как --with-nth, так и --preview, а <tab> играет роль разделителя.

echo -e 'first line\tfirst preview\nsecond line\tsecond preview' |    fzf --delimiter='\t' --with-nth=1 --preview='echo {2}'

fzf разбивает каждую строку по символу табуляции; опция --with-nth=1 указывает fzf показать первую часть в окне выбора; {2} в команде предварительного просмотра будет заменена второй частью, и так как она передаётся в echo, то просто отобразится.

Пример работы с полями в fzfПример работы с полями в fzf

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

function pr-checkout() {  local jq_template pr_number  jq_template='"'\'#\(.number) - \(.title)'\'\t'\'Author: \(.user.login)\n'\'Created: \(.created_at)\n'\'Updated: \(.updated_at)\n\n'\'\(.body)'\'"'  pr_number=$(    gh api 'repos/:owner/:repo/pulls' |    jq ".[] | $jq_template" |    sed -e 's/"\(.*\)"/\1/' -e 's/\\t/\t/' |    fzf \      --with-nth=1 \      --delimiter='\t' \      --preview='echo -e {2}' \      --preview-window=top:wrap |    sed 's/^#\([0-9]\+\).*/\1/'  )  if [ -n "$pr_number" ]; then    gh pr checkout "$pr_number"  fi}

Мы немного изменили эту простую функцию. Извлекли шаблон строки jq в переменную, а затем дополнили её информацией об авторе, времени создания пул-реквеста, времени его последнего обновления, а также его описанием . Всю эту информацию мы получили в объекте JSON. Ответьте на этот запрос к api гитхаба: gh api 'repos/:owner/:repo/pulls'.

Обратите внимание, что мы отделили новую информацию номер и заголовок символом табуляции \t. Символ табуляции используется также в качестве разделителя в fzf, затем мы показываем номер пул-реквеста и заголовок в окне выбора (при помощи --with-nth=1), а оставшуюся информацию показываем в окне предварительного просмотра (при помощи --preview='echo -e {2}').

Обратите внимание также, что на этот раз в jq мы не используем опцию --raw-output. Причина немного неочевидна. Строки, которые мы создаём с помощью jq, содержат экранированные символы новой строки. Если мы передадим опцию --raw-output в jq, она будет интерпретировать все экранированные символы, и, в частности, вместо \n отобразится именно новая строка. Вот пример, сравните выходные данные этой команды:

echo '{}' | jq --raw-output '"first\nsecond"'

и команды

echo '{}' | jq '"first\nsecond"'

первая выведет

firstsecond

А вторая вот такую строку:

"first\nsecond"

Первая версия проблематична. Помните, что fzf работает со строками, делает список строк, позволяя пользователю выбрать одну или несколько строк и вывести их. Это означает, что без опции сырого вывода каждый пул-реквест в fzf будет показан как множество строк. И это определённо не то, чего мы хотим. Поэтому позволим jq вывести escape-версию, чтобы гарантировать, что каждый пул-реквест это одна строка.

Однако такой подход вводит новые проблемы, первая мы по-прежнему хотим настоящие символы новой строки, а не символы \n. Эта проблема решается командой echo -e, которая включает интерпретацию escape-символов. Вторая проблема в том, что без опции сырого вывода jq в начале и в конце строки показывает символы кавычек и распечатывает наш разделитель, то есть табуляцию, как символ в escape. Эту проблему мы решим удалением кавычек в ручном режиме и заменой первого escape-символа \t на настоящую табуляцию. Именно это делается в sed после jq.

Наконец, обратите внимание, что мы определили опцию --preview-window=top:wrap, чтобы fzf оборачивал строки в окне предпросмотра и отображал их верхней части экрана, а не справа.

И вот как это выглядит в действии:

Создание веток для фич из проблем (issues) в JIRA

Мы видели выше, как использовать fzf для удаления ветвей git. Теперь давайте посмотрим на противоположную задачу создание новых ветвей. На работе для отслеживания проблем мы используем JIRA. Каждая ветвь функции обычно соответствует какой-то проблеме JIRA. Чтобы поддерживать эту взаимосвязь, я использую схему именования ветвей git, о которой расскажу ниже. Предположим, что проект JIRA называется BLOG, и сейчас я работаю над проблемой BLOG-1232 с названием Добавить в сценарий запуска флаг вывода подробностей. Я называю свою ветку BLOG-1232/add-a-verbose-flag-to-the-startup-script; описание обычно даёт достаточно информации, чтобы определить функцию, которой соответствует ветвь, а часть BLOG-1232 позволяет мне перейти к тикету JIRA, когда я ищу подробности о проблеме.

Вполне понятно, как выглядит рабочий процесс создания этих веток:

  • вы открываете issue из JIRA в браузере;

  • копируете номер проблемы или запоминаете его;

  • переключаетесь на терминал, начинаете вводить git checkout -b BLOG-1232/;

  • переключаетесь на браузер и смотрите на название;

  • переключаетесь на терминал и добавляете похожее на название в JIRA описание в kebab-cased.

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

И это ещё один рабочий процесс, который можно полностью автоматизировать. С проблемами в Jira можно работать так же, как мы работали с пул-реквестами, через API JIRA. Функция, которую мы напишем, подобна pr-checkout, но будет иметь несколько заметных отличий от неё.

Во-первых, от жира нет удобного инструмента, подобного gh, чтобы общаться с её api. Во-вторых, сервер (по крайней мере сервер, с которым работаю я) не разрешает создавать токены доступа, что заставляет меня при доступе к api использовать простые имя пользователя и пароль. Мне не хочется сохранить мой пароль в скрипте оболочки, а точнее, не хочется делать это в незашифрованном файле, поэтому, чтобы пароль хранился безопаснее, воспользуемся secret-tool. Наконец, создание имени ветки требует большего, чем простое извлечение текста; воспользуемся комбинаций cut, sed, и awk.

Давайте сначала посмотрим на скрипт, а потом попробуем понять, как он работает.

function create-branch() {  # The function expectes that username and password are stored using secret-tool.  # To store these, use  # secret-tool store --label="JIRA username" jira username  # secret-tool store --label="JIRA password" jira password  local jq_template query username password branch_name  jq_template='"'\'\(.key). \(.fields.summary)'\'\t'\'Reporter: \(.fields.reporter.displayName)\n'\'Created: \(.fields.created)\n'\'Updated: \(.fields.updated)\n\n'\'\(.fields.description)'\'"'  query='project=BLOG AND status="In Progress" AND assignee=currentUser()'  username=$(secret-tool lookup jira username)  password=$(secret-tool lookup jira password)  branch_name=$(    curl \      --data-urlencode "jql=$query" \      --get \      --user "$username:$password" \      --silent \      --compressed \      'https://jira.example.com/rest/api/2/search' |    jq ".issues[] | $jq_template" |    sed -e 's/"\(.*\)"/\1/' -e 's/\\t/\t/' |    fzf \      --with-nth=1 \      --delimiter='\t' \      --preview='echo -e {2}' \      --preview-window=top:wrap |    cut -f1 |    sed -e 's/\. /\t/' -e 's/[^a-zA-Z0-9\t]/-/g' |    awk '{printf "%s/%s", $1, tolower($2)}'  )  if [ -n "$branch_name" ]; then    git checkout -b "$branch_name"  fi}

В скрипте мы видим три части. Первая часть это команда curl, её переменные. Через них скрипт общается с API JIRA. Затем вывод api конвертируется строки формата, удобного для fzf; это часть скрипта такая же, как у pr-checkout. Наконец, вывод fzf конвертируется формат имени ветки.

Самые существенные изменения в сравнении с pr-checkout эта команда curl. Мы воспользовались конечной точкой поиска JIRA, которая в качестве параметра URL ожидает запрос на языке JQL. В моём случае меня интересуют все проблемы проекта BLOG, которые закреплены за мной, и те, что отмечены строкой In Progress. Строка запроса JQL содержит пробелы, знаки и скобки. Все они недопустимы в url, поэтому их нужно закодировать. Опция curl --data-urlencode автоматически закодирует эти символы. Поскольку в этой опции по умолчанию применяется запрос POST, чтобы переключиться на get, мы должны добавить опцию --get. Также воспользуемся опцией --user, чтобы сообщить curl, что нужно добавить заголовок базовой аутентификации. И последнее: добавим опцию --silent, чтобы опустить информацию о прогрессе выполнения и --compressed, чтобы сэкономить на пропускную способность.

Затем, чтобы конвертировать записи массива в JSON ответе в одну строку, воспользуемся той же техничкой, что и выше, разделив строку поиска в окне предпросмотра по символу табуляции и пропустив вывод через fzf, чтобы позволить пользователю выбрать запись. Вывод fzf будет строкой вроде BLOG-1232. Add a verbose flag to the startup script{...preview part}, чтобы удалить часть предварительного просмотра строки, воспользуемся командой cut. По умолчанию cut в качестве разделителя использует символ табуляции, а опция -f1 сообщает cut, что нужно вывести первое поле. Результат выполнения команды будет таким: BLOG-1232. Add-a-verbose-flag-to-the-startup-scrip. Затем команда sed заменит первую точку на символ табуляции, а все нечисловые и неалфавитные символы на -, сохранив при этом наши табуляции. И вот результат: BLOG-1232<tab>Add-a-verbose-flag-to-the-startup-script. Наконец, awk возьмёт строку, разделит её по табуляции, преобразует её вторую часть в нижний регистр и вернёт обе части символом косой черты в качестве разделителя.

 Создание новой ветки из проблем в JIRA Создание новой ветки из проблем в JIRA

Заключение

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

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

Узнайте, как прокачаться в других специальностях или освоить их с нуля:

Другие профессии и курсы
Подробнее..

Резервное копирование конфигурации ресурсов в Kubernetes

19.02.2021 12:19:30 | Автор: admin

Администраторы кластеров kubernetes сталкиваются с задачей сохранить конфигурацию ресурсов из пространства имен и перенести в другой кластер, или же сделать резервную копию нестабильной тестовой площадки. С этой задачей без проблем справляется бегло написанный в терминале односторчный скрипт с утилитой kubectl, но что если надоело каждый раз тратить пару минут времени на очередное написание скрипта. Так и появилась утилита kube-dump, по сути это утилита которая умеет только одно - дампить ресурсы кластера.

пример работы утилиты для сохранения всех ресурсов одного неймспейсапример работы утилиты для сохранения всех ресурсов одного неймспейса

При помощи данной утилиты вы можете сохранить ресурсы кластера в виде чистого yaml манифеста без лишних метаданных.

Ключевые особенности:

  • Сохранение выполняется только для тех ресурсов, к которым у вас есть доступ на чтение.

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

  • К сохранению подлежат как ресурсы пространств имен, так и глобальные ресурсы кластера.

  • Использовать утилиту можно локально как обычный скрипт или запустить в контейнере или в кластере kubernetes к примеру как CronJob.

  • Может создавать архивы и ротировать их за собой.

  • Может фиксировать состояние в git репозитории и отправлять в удаленный репозиторий.

  • Вы можете указать конкретный перечень ресурсов кластера для выгрузки.

Конфигурацию для утилиты передаем с помощью аргументов командной строки или при помощью экспортированных переменных окружений или прочитанных из .env файла.

Начало работы

Необходимый минимум для запуска это наличие установленного kubectl и подключенного в него кластера, а также утилит jq и yq. Более подробно описано на странице документации по локальному запуску, а аргументы командной строки описаны здесь или же доступны по ключу --help.

Запустим утилиту для сохранения всех ресурсов кластера:

./kube-dump dump

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

Получим свежий образ и запустим утилиту для сохранения пространств имен dev и prod в смонтированную директорию /dump, где для доступа к кластеру мы пробрасываем в контейнер свой конфигурационный файл от kubectl.

docker pull woozymasta/kube-dump:latestdocker run --tty --interactive --rm \  --volume $HOME/.kube:/.kube \  --volume $HOME/dump:/dump \  woozymasta/kube-dump:latest \  dump-namespaces -n dev,prod -d /dump --kube-config /.kube/config

Установка CronJob в кластер

Рассмотрим более сложный пример когда контейнер будет запущен в кластере как CronJob который будет каждую ночь снимать дамп ресурсов и фиксировать правки в git репозитории с последующей отправкой в удаленный репозиторий. Пример подробно описан в статье.

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

Создадим пространство имен где будет работать наш CronJob и ServiceAccount который будет использовать ClusterRoleBinding для роли view:

kubectl create ns kube-dumpkubectl -n kube-dump apply -f \  https://raw.githubusercontent.com/WoozyMasta/kube-dump/master/deploy/cluster-role-view.yaml

В качестве примера будем использовать авторизацию в GitLab по OAuth токену, по этому создадим секрет с адресом репозитория и токеном для авторизации:\

kubectl -n kube-dump create secret generic kube-dump \  --from-literal=GIT_REMOTE_URL=https://oauth2:$TOKEN@corp-gitlab.com/devops/cluster-bkp.git

Перед установкой настройте переменные окружения под ваши нужды, в примере по умолчанию установлен режим копирования пространств имен dev и prod с последующей фиксацией изменений в ветке my-cluster и отправкой в удаленный репозиторий.

Настроим CronJob в которм укажем периодичность запуска задания:

spec:  schedule: "0 1 * * *"

Или же установите пример как есть и после отредактируйте его:

kubectl -n kube-dump apply -f \  https://github.com/WoozyMasta/kube-dump/blob/master/deploy/cronjob-git-token.yamlkubectl -n kube-dump edit cronjobs.batch kube-dump

Планы по дальнейшему развитию

  • Реализовать отправку дампов в s3 совместимое хранилище;

  • Отправка уведомлений по электронной почте и через веб-хук;

  • Git-crypt для шифрования чувствительных данных;

  • Автодополнение в Bash/Zsh;

  • Поддержка OpenShift.

Также буду рад Вашим комментариям и предложениям с идеями и критикой.

Ссылки

Подробнее..

Локализация своих скриптов на BASH

05.02.2021 14:16:53 | Автор: admin

Создание меню на BASH задача сама по себе не сложная: "case тебе в руки и echo в спину". Решая её в очередной раз, мне захотелось добавить возможность отображать текст на других языках. Осталось решить, как сделать сам процесс локализации меню более удобным. Если оно большое, то решение "в лоб" превратит его в громоздкую копипасту. Здесь я хотел бы поделиться тем, как решил эту проблему для себя. Надеюсь для кого то это будет не безынтересным.



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


Примечание:

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


Мне не хотелось бы разбивать скрипт на несколько частей и хранить локализацию в отдельных файлах. Скрипт тем и удобен, что его проще использовать одним файлом. Поэтому тексты будем хранить в массивах.


Реализация будет состоять из:


  • буквенного кода языка
  • массива слов
  • преобразователя косвенных ссылок для обращения к массиву
  • обращения к элементам массива
  • создания меню

Теперь рассмотрим подробнее


Зададим язык, добавив короткий буквенный код языка (ru, en), для начала английский:


langset=en

Добавим массивы слов и предложений (по количеству языков):


language_en=( "English" "Quit" "Hi, Habr!" )language_ru=( "Русский" "Выход" "Привет, Хабр!" )

Создадим косвенную ссылку и сформируем из неё новый массив в переменной lng:


lng="language_$langset[@]"; lng=("${!lng}")

То есть здесь для переменной lng создаётся значение, состоящее из части имени массива со словами и кода заданного языка из переменной langset. Далее создаётся массив, который при langset=en будет равен language_en, а при langset=ru будет равен language_ru.
Если языков будет много, то такой подход позволит избавиться от многочисленных if-elif или case. Чтобы изменить язык, достаточно будет добавить массив с переводом и установить язык в переменной langset.


Запустим всё это в консоли:


language_en=( "English" "Quit" "Hi, Habr!" )language_ru=( "Русский" "Выход" "Привет, Хабр!" )langset=enlng="language_$langset[@]"; lng=("${!lng}")echo "${lng[2]}"# Вывод: Hi, Habr!langset=rulng="language_$langset[@]"; lng=("${!lng}")echo "${lng[2]}"# Вывод: Привет, Хабр!

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


Построение меню


А теперь попробуем создать скрипт.
Для примера я взял меню управления командами Git через CLI (command line interface). Это немного надуманно, но хорошо подходит для примера, так как в Git много команд и как раз можно построить единое по своей задаче меню с множеством параметров.


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


#!/bin/bash# Код языкаlangset=en# Пункты менюlanguage_en=( "English" "Quit" "Main menu" "Git: add ALL files/commit" "Git init" "Change language" "Language selection" )message_en=( "English" "Select item" "Wrong! This item does not exist" "Added all files" "Enter you commit" "Changes recorded" "Select a language" "The language has been changed to" "Start the program again" "Menu for language change" )language_ru=( "Русский" "Выход" "Основное меню" "Git: добавить ВСЕ файлы/коммит" "" "" "Выбор языка" )message_ru=( "Русский" "Выберите пункт" "Неверно! Этого пункта не существует" "Добавление всех файлов" "Введите ваш коммит" "Изменения зарегистрированы" "Выберите язык" "Язык изменен на" "Запустите программу заново" "Меню для смены языка" )language_de=( "Deutsch" )message_de=( "Deutsch" "" "" "" "" "" "" "" "Starten Sie das Programm neu" )

Пройдемся по массивам и создадим новые для lng и msg, которые и будут использоваться:


languages() {    lng="language_$langset[@]"; lng=("${!lng}")    msg="message_$langset[@]"; msg=("${!msg}")    for b in ${!language_en[@]} ${!message_en[@]} ; do        if [[ ! ${lng[$b]} ]] ; then            lng[$b]=${language_en[$b]}        fi        if [[ ! ${msg[$b]} ]] ; then            msg[$b]=${message_en[$b]}        fi    done}languages

Чтобы при необходимости иметь возможность применять языковые настройки (это пригодится в следующей статье), заключим всё в функцию и тут же вызовем её.


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


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


Теперь, когда с языками разобрались, приступим к построению меню. Тут всё стандартно: бесконечный цикл, внутри него цикл для вывода элементов на экран и выбор вариантов через case.


Формируем массив для вывода на экран. Только те пункты, которые нам нужны:


local menu0=([1]="${lng[3]}" "${lng[4]}" "${lng[5]}" "${lng[1]}")

Здесь делаем индексацию с 1, чтобы выводить индекс как соответствующий пункт меню.


Добавляем шапку, пробегаемся по индексам через ${!menu0[@]} и выводим на экран:


echoecho "---${lng[2]}---"for op in "${!menu0[@]}" ; do     echo "$op ) ${menu0[$op]}"doneecho ---------- 

Далее предложим пользователю выбрать необходимый пункт. Будем ожидать нажатие цифровой клавиши через read -s -n1 -p. Где -s не отображать введённые данные (чаще используется для ввода паролей). Мы же потом сами отобразим введеный текст в более удобном формате. -p строка приглашения для вывода подсказки. -n1 параметр, считывающий число симмволов ввода. Здесь мы заранее знаем, что пунктов меню будет не более 9 (то есть числа из одной цифры), поэтому при нажатии одного любого символа программа продолжит работу дальше. Не надо будет нажимать Enter для ввода. Немного непривычно, поэтому можно убрать.


# read со строкой приглашенияread -s -n1 -p "${msg[1]}: " item#вывод нажатой клавишиecho "[$item]->: ${menu0[$item]}"

Ну и заключительный оператор выбора case с обработкой введенного значения:


case $item in        # Команды Git, без обработки ошибок (репозитория ведь может и не быть)    1 )     git add .        read -p "${msg[4]}: " comm        git commit -m "$comm"        echo "${msg[5]}" ;;    2 ) git init ;;    3 ) echo "${msg[9]}" ;;        # Выход    4 ) exit ;;        # Обработка остальных клавиш и вывод сообщения об ошибке    * ) echo "[$item]->: ${msg[2]}"; sleep 2 ;;esac        

Здесь case сделан упрощенным, только для примера. В следующей статье я буду использовать его в более универсальном варианте.


Весь код
#!/bin/bash# Код языкаlangset=ru# Меню и сообщенияlanguage_en=( "English" "Quit" "Main menu" "Git: add ALL files/commit" "Git init" "Change language" "Language selection" )message_en=( "English" "Select item" "Wrong! This item does not exist" "Added all files" "Enter you commit" "Changes recorded" "Select a language" "The language has been changed to" "Start the program again" "There will be a menu for changing the language" )language_ru=( "Русский" "Выход" "Основное меню" "Git: добавить ВСЕ файлы/коммит" "" "" "Выбор языка" )message_ru=( "Русский" "Выберите пункт" "Неверно! Этого пункта не существует" "Добавление всех файлов" "Введите ваш коммит" "Изменения зарегистрированы" "Выберите язык" "Язык изменен на" "Запустите программу заново" "Здесь будет меню для смены языка" )language_de=( "Deutsch" )message_de=( "Deutsch" "" "" "" "" "" "" "" "Starten Sie das Programm neu" )languages() {    # Косвенные ссылки и создание нового массива    lng="language_$langset[@]"; lng=("${!lng}")    msg="message_$langset[@]"; msg=("${!msg}")    # Сравнение массивов для проверки на пропущенные элементы    for b in ${!language_en[@]} ${!message_en[@]} ; do        if [[ ! ${lng[$b]} ]] ; then            lng[$b]=${language_en[$b]}        fi        if [[ ! ${msg[$b]} ]] ; then            msg[$b]=${message_en[$b]}        fi    done}languagesmain() {    # Создание и вывод меню на экран    local menu0=([1]="${lng[3]}" "${lng[4]}" "${lng[5]} [$langset]" "${lng[1]}")    while true ; do         echo        echo "---${lng[2]}---"        for op in "${!menu0[@]}" ; do             echo "$op ) ${menu0[$op]}"        done        echo ----------        # Ожидание ввода значения        read -s -n1 -p "${msg[1]}: " item        echo "[$item]->: ${menu0[$item]}"        # Оператор выбора        case $item in                # Команды Git, без обработки ошибок (репозитория ведь может и не быть)            1 ) #               git add .                read -p "${msg[4]}: " comm#               git commit -m "$comm"                echo "${msg[5]}" ;;            2 ) #               git init ;;            3 ) echo "${msg[9]}" ;;                # Выход            4 ) exit ;;                # Обработка остальных клавиш и вывод сообщения об ошибке            * ) echo "[$item]->: ${msg[2]}"; sleep 2 ;;        esac                done}mainexit 0

Это первая статья из двух. В следующей рассмотрим создание многоуровнего меню, разукрасим вывод (как же без этого), а также попробуем сохранить настройки, то есть задать код языка прямо из меню, не открывая Shell-скрипт в текстовом редакторе.


Спасибо, что дочитали до конца. Надеюсь, было не скучно :)

Подробнее..

Локализация своих скриптов на BASH, часть 2

13.02.2021 16:10:43 | Автор: admin

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

Создание меню

В прошлой статье для вывода меню на экран и выбора необходимого пункта мы использовали цикл for и оператор выбора case. Очевидно, что для создания многоуровнего меню эти шаги нужно будет повторить несколько раз, то есть для каждого подменю. При этом придется заново создавать шапку меню, внешний вид и так далее.
Хороший способ этого избежать - вынести for, case и read в отдельную функцию (назовём её prints), а далее будем просто передавать в неё необходимые параметры. Все сценарии, которые будут выполнятся при выборе тех или иных пунктов меню также будут вынесены в соответствующие функции.

Так, чтобы добавить в скрипт новое действие:

  • добавляем в языковой массив слова и фразы

  • в массив с основным или дополнительным меню вставляем соответствующий пункт и команду вызова функции

  • добавляем функцию с необходимым фрагментом кода

Для начала создадим основное меню, которое сразу появится на экране при запуске скрипта:

menu0=("${lng[3]};main" "${lng[4]};gitadd" "${lng[5]};gitinit" "${lng[2]};options" "${lng[1]};exit")

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

Поместим массив в функцию main:

main() {    # Массив с основным меню    local menu0=("${lng[3]};main" "${lng[4]};gitadd" "${lng[5]};gitinit" "${lng[2]};options" "${lng[1]};exit")    while true ; do        # Передаем массив в функцию вывода на экран        # Вторым аргументом идет сообщение, которое отобразится в строке приглашения        prints "menu0[@]" "${msg[1]}"    done}main

При таком подходе создание дополнительных меню немного упрощается, например options:

options() {    local menu1=("${lng[2]};options" "${lng[7]} [$langset];langmenu" "${lng[1]};exit")    prints "menu1[@]" "${msg[1]}"

Теперь пришло время рассмотреть функцию prints, которая выводит все эти меню на экран. Сначала поместим в неё конструкцию, разделяющую элемент на две части. Для разделения задействуем команду вырезания данных cut:

if [[ "$1" == "text" ]] ; then    # Для текста меню до разделителя    echo "$2" | cut -d ";" -f 1    returnelif [[ "$1" == "command" ]] ; then    # Для команды после разделителя    echo "$2" | cut -d ";" -f 2    returnfi

Для получения, например, текстового поля вызывать её будем командой ${prints "text" "${menu[0]}"} , где второй аргумент - сам элемент массива.

Небольшое, но важное отступление: В скрипт я добавил возможность раскрашивать вывод на экран в разные цвета. Отвечающий за это код я поместил в функцию colors. Для раскрашивания используются ANSI escape последовательности (вывод echo -e) с расширенной палитрой на 256 цветов.

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

Я не буду описывать эти функции, но они будут в конечном коде с подробными комментариями.

Продолжим рассматривать функцию вывода меню на экран prints. Задаем массив из массива, переданного в функцию через аргумент:

local menu=("${!1}")

Далее - непосредственно вывод на экран. Выводим дату и текущий путь, затем название меню желтым цветом, название берется из текстовой части 1 элемента массива:

pwdscolors "title" "---$(prints "text" "${menu[0]}")---"

Перебираем в цикле for массив, выводим пункты белым цветом, ожидаем ввод значения read и обрабатываем нажатие через оператор выбора case:

for (( op=1; op < "${#menu[@]}"; op++ )); docolors "item" "$op ) $(prints "text" "${menu[$op]}")"doneecho ----------read -s -n1 -p "$(colors "item" "$2: ")" itemcase $item in[1-$((${#menu[@]}-1))] ) # Вывод выбранного пункта меню зеленым цветомcolors "ok" "[$item]->: $(prints "text" "${menu[$item]}")"# Вызов функции с фрагментом кода$(prints "command" "${menu[$item]}") ;;# Немедленное завершение по [q]"q" ) echo; exit;;# Обработка остальных клавиш и вывод сообщения об ошибке красным цветом* ) colors "err" "[$item]->: ${msg[2]}"; sleep 2 ;;esac
Так будет выглядеть это менюТак будет выглядеть это меню

Построение меню выбора языка

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

Итак, для того, чтобы выбирать язык через меню, составим список встроенных языков. Сначала создадим новую функцию langmenu.

Заранее хочу отметить, что мы не знаем количество и какие языки присутствуют в скрипте. Мы просто ищем их все. Для этого пробегаемся по текущему скрипту, через sed и регулярное выражение находим все имена массивов language_ и добавляем коды языков в массив. То есть из language_ru выкусываем ru:

# [-r] - расширенный синтаксис регулярных выражений# [-n] - вывод только того, что совпадает с шаблономlocal lng_sfx=($(sed -r -n "s/^\s?+language_(\w+)=.*/\1/p" "${0}"))

Учитывая, что мы не знали какие языки у нас будут, мы не могли сразу задать массив для передачи его в prints. Нужно будет собрать его по частям. Сделаем это там же в langmenu:

langmenu(){    local lng_sfx=($(sed -r -n "s/^\s?+language_(\w+)=.*/\1/p" "${0}"))    local menu2=("${lng[7]};langmenu")    for a in ${lng_sfx[@]} ; do        local d="language_$a[@]"; d=("${!d}")        menu2+=("$d;languages set $a")    done    menu2+=("${lng[1]};exit")    prints "menu2[@]" "${msg[6]}"}

Здесь мы:

  1. Создаем массив для вывода меню

  2. В цикле перебираем массив с кодами языков. На каждой итерации создаем косвенную ссылку, чтобы обратиться к 0 элементу соответствующего языкового массива (с записанным нзванием языка). На следующем шаге мы формируем элемент меню: в каждый элемент в текстовую часть добавляем название языка, а через разделитель ;, в командную часть, добавляем команду вызова функции languages и в качестве аргумента ставим код языка. Для английского языка получится "English;languages set en", где set en - аргументы для функции languages.

  3. После цикла добавляем в массив с меню команду выхода

  4. Передаем сформированный массив с языковым меню в функцию вывода на экран prints. Вторым аргументом идет сообщение, которое отобразится в строке приглашения

Меню выбора языкаМеню выбора языка

Сохранение языка через настройки

После выбора языка через меню запомним его через перезапись в скрипте. применим для этого потоковый редактор sed с ключами -i и -r, где -i - редактирование (перезапись) файла, -r - поддержка расширенного синтаксиса регулярных выражений.
Здесь всё просто: ищем первое вхождение строки, начинающейся с langset= и в ней переменную langset= с кодом языка через редактирование перезаписываем новым значением и сразу выходим:

sed -i -r "0,/^\s?+langset=/s/langset=[\"\']?\w*[\"\']?/langset=\"$langset\"/" "${0}"exit

В предыдущей части мы сформировали языковое меню и передали его в функцию prints. Которая, в свою очередь, через оператор выбора case вызывает функцию languages, описанную в прошлой статье и передаёт в неё аргумент set и код выбранного языка. Самое время добавить в languages функцию перезаписи настроек, а также вывести на экран сообщение о смене языка в ТЕКУЩЕЙ локализации. После этого применим новый язык и опять выведем то же самое сообщение на НОВОМ языке:

Функция languages
if [ "$1" == "set" ] ; then# Устанавливаем новый язык из входного аргументаlangset="$2"local df="language_$langset"echo# Сообщение на ТЕКУЩЕМ языке что язык изменен, цвет зеленыйcolors "ok" "${msg[7]} ${!df}. ${msg[8]}"# Применяем настройки языкаlanguages# Сообщение на НОВОМ языке что язык изменен, цвет зеленыйcolors "ok" "${msg[7]} ${lng[0]}. ${msg[8]}"echo# Перезаписываем переменную langset= с кодом языка и выходимsed -i -r "0,/^\s?+langset=/s/langset=[\"\']?\w*[\"\']?/langset=\"$langset\"/" "${0}"exit fi
Установка языкаУстановка языка

Заключение

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

Спасибо, что дочитали до конца.

Весь код с комментариями прикреплен ниже.

Весь код
#!/bin/bash# Код языкаlangset="ru"# Меню и сообщенияlanguage_en=( "English" "Quit" "Options" "Main menu" "Git: add ALL files/commit" "Git init" "Change language" "Language selection" )message_en=( "English" "Select item" "Wrong! This item does not exist" "Added all files" "Enter you commit" "Changes recorded" "Select a language" "The language has been changed to" "Start the program again" "Repository not found\nPlease, select Git init pepository" )language_ru=( "Русский" "Выход" "Настройки" "Основное меню" "Git: добавить ВСЕ файлы/коммит" "" "" "Выбор языка" )message_ru=( "Русский" "Выберите пункт" "Неверно! Этого пункта не существует" "Добавление всех файлов" "Введите ваш коммит" "Изменения зарегистрированы" "Выберите язык" "Язык изменен на" "Запустите программу заново" "Репозиторий не найден\nПожалуйста, инициализируйте репозиторий, выбрав Git init" )language_de=( "Deutsch" )message_de=( "Deutsch" "" "" "" "" "" "" "" "Starten Sie das Programm neu" )language_cn=( "" "" "" "")message_cn=( "" "" "" "" "" "" "" "" "" )# Settings sectionlanguages() {# Функция с языковыми настройками и установкой нового языка# Косвенные ссылки и создание нового массиваlng="language_$langset[@]"; lng=("${!lng}")msg="message_$langset[@]"; msg=("${!msg}")# Сравнение массивов для проверки на пропущенные элементыfor b in ${!language_en[@]} ${!message_en[@]} ; doif [[ ! ${lng[$b]} ]] ; thenlng[$b]=${language_en[$b]}fiif [[ ! ${msg[$b]} ]] ; thenmsg[$b]=${message_en[$b]}fidone# Установка нового языкаif [ "$1" == "set" ] ; then# Устанавливаем новый язык из входного аргументаlangset="$2"local df="language_$langset"# Выводим сообщение на ТЕУЩЕМ языке что язык изменен,# пишем какой выбрали, предлагаем перезапустить программуechocolors "ok" "${msg[7]} ${!df}. ${msg[8]}"# Применяем настройки языкаlanguages# Выводим сообщение на НОВОМ языке что язык изменен# пишем какой выбрали, предлагаем перезапустить программуcolors "ok" "${msg[7]} ${lng[0]}. ${msg[8]}"echo# Через регулярное выражение путем изменения файла# перезаписываем переменную langset= с кодом языка и выходим# [-r] - расширенный синтаксис регулярных выражений# [-i] - редактирование файла# [0,] - только первое вхождениеsed -i -r "0,/^\s?+langset=/s/langset=[\"\']?\w*[\"\']?/langset=\"$langset\"/" "${0}"exit fi}# Применяем настройки языкаlanguagescolors() {# Установка цвета текста и фона. Строки даны полностью,# чтобы можно было просто изменить цифры, ничего не дописывая# Здесь [48] - код расширенной палитры фона, [38] - текста# [5] - 8-битный формат цвета (0-255), [1] - жирный,# [22] - отменить жирный, [0] - сбросить все измененияcase "$1" in# Текст: темно-зеленый (часы)"tm" ) echo -e "\e[48;5;256;38;5;34;22m$2\e[0m" ;;# Фон: светло-синий, текст: белый жирный (часть полного пути)"pt" ) echo -e "\e[48;5;24;38;5;15;1m$2\e[0m" ;;# Текст: светло-желтый жирный (текущая папка)"cf" ) echo -e "\e[48;5;256;38;5;226;1m$2\e[0m" ;;# Текст: темно-зеленый жирный (цвет успешной операции)"ok" ) echo -e "\e[48;5;256;38;5;34;1m$2\e[0m" ;;# Текст: красный жирный (цвет ошибки)"err" ) echo -e "\e[48;5;256;38;5;160;1m$2\e[0m" ;;# Текст: светло-желтый (шапка меню)"title" ) echo -e "\e[48;5;256;38;5;226;22m$2\e[0m" ;;# Текст: белый (пункты меню и строка приглашения)"item" ) echo -e "\e[48;5;256;38;5;15;22m$2\e[0m" ;;esac}pwds() {# Цветное отображение полного пути текущей директории и датыecho echo ----------echo "$(colors 'tm' "[$(date +"%T")]") $(colors 'pt' "${PWD%/*}"/)$(colors 'cf'  "$(basename   "$PWD")")"echo ----------}prints() {# Функция вывода меню на экран# Разделение элемента массива на текст и команду, в качестве разделителя [;]if [[ "$1" == "text" ]] ; thenecho "$2" | cut -d ";" -f 1returnelif [[ "$1" == "command" ]] ; thenecho "$2" | cut -d ";" -f 2returnfi# Задаем массив из массива, переданного в функцию через аргументlocal menu=("${!1}")# Вывод даты и текущего путиpwds# Вывод названия меню желтым цветом, название берется# из текстовой части 1 элемента массива colors "title" "---$(prints "text" "${menu[0]}")---"# Вывод меню на экранfor (( op=1; op < "${#menu[@]}"; op++ )); do# Вывод пунктов меню белым цветом, названия берутся# из текстовой части соответствующего элемента массиваcolors "item" "$op ) $(prints "text" "${menu[$op]}")"doneecho ----------# Ожидание ввода значения, приглашение выводится белым цветомread -s -n1 -p "$(colors "item" "$2: ")" item# Оператор выбораcase $item in# Все числа от 1 до размера всего массива минус 1 (так как индексация массива с 0)# Вывод выбранного пункта меню зеленым цветом название берется# из текстовой части соответствующего элемента массива[1-$((${#menu[@]}-1))] ) colors "ok" "[$item]->: $(prints "text" "${menu[$item]}")"# Вызов функции с фрагментом кода, имя функции берется# из командной части соответствующего элемента массива$(prints "command" "${menu[$item]}") ;;# Немедленное завершение по [q]"q" ) echo; exit;;# Обработка остальных клавиш и вывод сообщения об ошибке красным цветом* ) colors "err" "[$item]->: ${msg[2]}"; sleep 2 ;;esac}# Application sectiongitinit() {# Для примера: фрагмент кода для [git init]git init}gitadd() {# Для примера: фрагмент кода для [git add] - добавить все файлыgit add .# Обработка ошибок. Если статус завершения команды не равен [0]# вывести сообщение об ошибке красным цветом и вернуться в менюif [[ "$?" != "0" ]] ; thencolors "err" "${msg[9]}" sleep 1return 1 fiecho "${msg[3]} ..."# Приглашение и ввод коммитаread -p "$(colors "item" "${msg[4]}: ")" commgit commit -m "$comm"# сообщение о завершении операции зеленим цветомcolors "ok" "${msg[5]}"}# Menu sectionlangmenu() {# Функция создания языкового меню# Проходим по текущему скрипту, через регулярное выражение находим# все имена массивов [language_*] и добавляем коды языков в массив# [-r] - расширенный синтаксис регулярных выражений# [-n] - вывод только того, что совпадает с шаблономlocal lng_sfx=($(sed -r -n "s/^\s?+language_(\w+)=.*/\1/p" "${0}"))# Создаем массив для вывода менюlocal menu2=("${lng[7]};langmenu")# Перебираем в цикле массив с кодами языков, на каждой итерации создаем косвенную ссылку,# чтобы обратиться к 0 элементу соответствующего языкового массива (с записанным нзванием языка)for a in ${lng_sfx[@]} ; dolocal d="language_$a[@]"; d=("${!d}")# Продолжаем формирование массива для вывода языкового меню# В каждый элемент в текстовую часть добавляем название языка, а через# разделитель [;], в командную часть, добавляем команду вызова функции# [languages] и в качестве аргумента ставим код языка. Для английского языка# получится ["English;languages set en"], где [set en] - аргументы для функции [languages]menu2+=("$d;languages set $a")done# Добавляем в меню команду выходаmenu2+=("${lng[1]};exit")# Передаем сформированный массив с языковым меню в функцию вывода на экран# Вторым аргументом идет сообщение, которое отобразится в строке приглашенияprints "menu2[@]" "${msg[6]}"}options() {# Функция создания меню с настройками# В каждый элемент в текстовую часть добавляем название необходимого пункта меню,# а через разделитель [;], в командную часть, добавляем соответствующую команду вызова функцииlocal menu1=("${lng[2]};options" "${lng[7]} [$langset];langmenu" "${lng[1]};exit")# Передаем массив в функцию вывода на экран# Вторым аргументом идет сообщение, которое отобразится в строке приглашенияprints "menu1[@]" "${msg[1]}"}main() {# Функция создания основного меню# В каждый элемент в текстовую часть добавляем название необходимого пункта меню,# а через разделитель [;], в командную часть, добавляем соответствующую команду вызова функции# Здесь и в других массивах, содержащих меню, первая запись - это название и команда вызова текущей функции# Необходимо для того, чтобы передавать название меню для печати шапки и для вызова этой функции при необходимостиlocal menu0=("${lng[3]};main" "${lng[4]};gitadd" "${lng[5]};gitinit" "${lng[2]};options" "${lng[1]};exit")while true ; do# Передаем массив в функцию вывода на экран# Вторым аргументом идет сообщение, которое отобразится в строке приглашенияprints "menu0[@]" "${msg[1]}"done}mainexit 0
Подробнее..

Основы Bash-скриптинга для непрограммистов. Часть 2

30.01.2021 20:16:42 | Автор: admin

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

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

Скрипты

Для выполнения нескольких команд одним вызовом удобно использовать скрипты. Скрипт это текстовый файл, содержащий команды для shell. Это могут быть как внутренние команды shell, так и вызовы внешних исполняемых файлов.

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

Перейдем в домашнюю директорию командой cd ~ и создадим в ней с помощью редактора nano (nano script.sh)файл, содержащий 2 строки:

#!/bin/bashecho Hello!

Чтобы выйти из редактора nano после набора текста скрипта, нужно нажать Ctrl+X, далее на вопрос "Save modified buffer?" нажать Y, далее на запрос "File Name to Write:" нажать Enter. При желании можно использовать любой другой текстовый редактор.

Скрипт запускается командой ./<имя_файла>, т.е. ./ перед именем файла указывает на то, что нужно выполнить скрипт или исполняемый файл, находящийся в текущей директории. Если выполнить команду script.sh, то будет выдана ошибка, т.к. оболочка будет искать файл в директориях, указанных в переменной среды PATH, а также среди встроенных команд (таких, как, например, pwd):

test@osboxes:~$ script.shscript.sh: command not found

Ошибки не будет, если выполнять скрипт с указанием абсолютного пути, но данный подход является менее универсальным: /home/user/script.sh. Однако на данном этапе при попытке выполнить созданный файл будет выдана ошибка:

test@osboxes:~$ ./script.sh-bash: ./script.sh: Permission denied

Проверим права доступа к файлу:

test@osboxes:~$ ls -l script.sh-rw-rw-r-- 1 test test 22 Nov  9 05:27 script.sh

Из вывода команды ls видно, что отсутствуют права на выполнение. Рассмотрим подробнее на картинке:

Права доступа задаются тремя наборами: для пользователя, которому принадлежит файл; для группы, в которую входит пользователь; и для всех остальных. Здесь r, w и x означают соответственно доступ на чтение, запись и выполнение.

В нашем примере пользователь (test) имеет доступ на чтение и запись, группа также имеет доступ на чтение и запись, все остальные только на чтение. Эти права выданы в соответствии с правами, заданными по умолчанию, которые можно проверить командой umask -S. Изменить права по умолчанию можно, добавив вызов команды umask с нужными параметрами в файл профиля пользователя (файл ~/.profile), либо для всех пользователей в общесистемный профиль (файл /etc/profile).

Для того, чтобы установить права, используется команда chmod <параметры> <имя_файла>. Например, чтобы выдать права на выполнение файла всем пользователям, нужно выполнить команду:

test@osboxes:~$ chmod a+x script.sh

Чтобы выдать права на чтение и выполнение пользователю и группе:

test@osboxes:~$ chmod ug+rx script.sh

Чтобы запретить доступ на запись (изменение содержимого) файла всем:

test@osboxes:~$ chmod a-w script.sh

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

test@osboxes:~$ chmod 754 script.sh

Будут выданы права -rwxr-xr--:

test@osboxes:~$ ls -la script.sh-rwxr-xr-- 1 test test 22 Nov  9 05:27 script.sh

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

Символ перед наборами прав доступа указывает на тип файла ( означает обычный файл, d директория, l ссылка, c символьное устройство, b блочное устройство, и т. д.). Соответствие числа, его двоичного представления и прав доступ можно представить в виде таблицы:

Число

Двоичный вид

Права доступа

0

000

Нет прав

1

001

Только выполнение (x)

2

010

Только запись (w)

3

011

Запись и выполнение (wx)

4

100

Только чтение (r)

5

101

Чтение и выполнение (rx)

6

110

Чтение и запись (rw)

7

111

Чтение, запись и выполнение (rwx)

Выдав права на выполнение, можно выполнить скрипт:

test@osboxes:~$ ./script.shHello!

Первая строка в скрипте содержит текст #!/bin/bash. Пара символов #! называется Шебанг (англ. shebang) и используется для указания интерпретатору, с помощью какой оболочки выполнять указанный скрипт. Это гарантирует корректность исполнения скрипта в нужной оболочке в случае, если у пользователя будет указана другая.

Также в скриптах можно встретить строку #!/bin/sh. Но, как правило, /bin/sh является ссылкой на конкретный shell, и в нашем случае /bin/sh ссылается на /bin/dash, поэтому лучше явно указывать необходимый интерпретатор. Вторая строка содержит команду echo Hello!, результат работы которой мы видим в приведенном выводе.

Параметры скриптов

Для того, чтобы обеспечить некоторую универсальность, существует возможность при вызове передавать скрипту параметры. В этом случае вызов скрипта будет выглядеть так: <имя_скрипта> <параметр1> <параметр2> , например ./script1.sh Moscow Russia.

Для того, чтобы получить значение первого параметра, необходимо в скрипте указать $1, второго - $2, и т.д. Существует также ряд других переменных, значения которых можно использовать в скрипте:
$0 имя скрипта
$# количество переданных параметров
$$ PID(идентификатор) процесса, выполняющего скрипт
$? код завершения предыдущей команды

Создадим файл script1.sh следующего содержания:

#!/bin/bashecho Hello, $USER!printf "Specified City is: %s, Country is: %s\n" $1 $2

Выдадим права на выполнение и выполним скрипт с параметрами:

test@osboxes:~$ chmod u+x script1.shtest@osboxes:~$ ./script1.sh Moscow RussiaHello, test!Specified City is: Moscow, Country is: Russia

Мы передали 2 параметра, указывающие город и страну, и использовали их в скрипте, чтобы сформировать строку, выводимую командой printf. Также для вывода в строке Hello использовали имя пользователя из переменной USER.

Для того, чтобы передать значения параметров, состоящие из нескольких слов (содержащие пробелы), нужно заключить их в кавычки:

test@osboxes:~$ ./script1.sh "San Francisco" "United States"Hello, test!Specified City is: San Francisco, Country is: United States

При этом нужно доработать скрипт, чтобы в команду printf параметры также передавались в кавычках:

printf "Specified City is: %s, Country is: %s\n" "$1" "$2"

Из приведенных примеров видно, что при обращении к переменной для получения её значения используется символ $. Для того, чтобы сохранить значение переменной просто указывается её имя:

COUNTRY=RUSSIAecho $COUNTRY

Операторы условного выполнения, выбора и циклы

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

Оператор условного выполнения представляет собой конструкцию вида:

if [ <условие> ]then  <команда1>else  <команда2>fi

Создадим скрипт, проверяющий длину введенной строки (например, для проверки длины пароля), которая должна быть не меньше (т.е. больше) 8 символов:

#!/bin/bashecho Hello, $USER!echo -n "Enter string: "read strif [ ${#str} -lt 8 ]then  echo String is too shortelse  echo String is okfi

Выполним 2 теста, с длиной строки 5 и 8 символов:

test@osboxes:~$ ./script2.shHello, test!Enter string: abcdeString is too shorttest@osboxes:~$ ./script2.shHello, test!Enter string: abcdefghString is ok

Командой read str мы получаем значение, введенное пользователем и сохраняем его в переменную str. С помощью выражения ${#str} мы получаем длину строки в переменной str и сравниваем её с 8. Если длина строки меньше, чем 8 (-lt 8), то выдаем сообщение String is too short, иначе String is ok.

Условия можно комбинировать, например, чтобы указать, чтоб длина должна быть не меньше восьми 8 и не больше 16 символов, для условия некорректных строк нужно использовать выражение [ ${#str} -lt 8 ] || [ ${#str} -gt 16 ]. Здесь || означает логическое "ИЛИ", а для логического "И" в bash используется &&.

Условия также могут быть вложенными:

#!/bin/bashecho Hello, $USER!echo -n "Enter string: "read strif [ ${#str} -lt 8 ]then  echo String is too shortelse  if [ ${#str} -gt 16 ]  then    echo String is too long  else    echo String is ok  fifi

Здесь мы сначала проверяем, что строка меньше 8 символов, отсекая минимальные значения, и выводим "String is too short", если условие выполняется. Если условие не выполняется(строка не меньше 8 символов) - идем дальше(первый else) и проверяем, что строка больше 16 символов. Если условие выполняется - выводим "String is too long", если не выполняется(второй else) - выводим "String is ok".

Результат выполнения тестов:

test@osboxes:~$ ./script2.shHello, test!Enter string: abcdefString is too shorttest@osboxes:~$ ./script2.shHello, test!Enter string: abcdefghijklmnopqrstuvString is too longtest@osboxes:~$ ./script2.shHello, test!Enter string: abcdefghijklString is ok

Оператор выбора выглядит следующим образом:

case "$переменная" in "$значение1" ) <команда1>;; "$значение2" ) <команда2>;;esac

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

#!/bin/bashecho -n "Enter the name of planet: "read PLANETecho -n "The $PLANET has "case $PLANET in  Mercury | Venus ) echo -n "no";;  Earth ) echo -n "one";;  Mars ) echo -n "two";;  Jupiter ) echo -n "79";;  *) echo -n "an unknown number of";;esacecho " satellite(s)."

Тест:

test@osboxes:~$ ./script3.shEnter the name of planet: MercuryThe Mercury has no satellite(s).test@osboxes:~$ ./script3.shEnter the name of planet: VenusThe Venus has no satellite(s).test@osboxes:~$ ./script3.shEnter the name of planet: EarthThe Earth has one satellite(s).test@osboxes:~$ ./script3.shEnter the name of planet: MarsThe Mars has two satellite(s).test@osboxes:~$ ./script3.shEnter the name of planet: JupiterThe Jupiter has 79 satellite(s).test@osboxes:~$ ./script3.shEnter the name of planet: Alpha555The Alpha555 has an unknown number of satellite(s).

Здесь в зависимости от введенного названия планеты скрипт выводит количество её спутников.
В case мы использовали выражение Mercury | Venus, где | означает логическое "ИЛИ" (в отличие от if, где используется ||), чтобы выводить "no" для Меркурия и Венеры, не имеющих спутников. В case также можно указывать диапазоны с помощью []. Например, скрипт для проверки принадлежности диапазону введенного символа будет выглядеть так:

#!/bin/bashecho -n "Enter key: "read -n 1 keyechocase "$key" in  [a-z]   ) echo "Lowercase";;  [A-Z]   ) echo "Uppercase";;  [0-9]   ) echo "Digit";;  *       ) echo "Something else";;esac

Мы проверяем символ на принадлежность одному из четырех диапазонов(английские символы в нижнем регистре, английские символы в верхнем регистре, цифры, все остальные символы). Результат теста:

test@osboxes:~$ ./a.shEnter key: tLowercasetest@osboxes:~$ ./a.shEnter key: PUppercasetest@osboxes:~$ ./a.shEnter key: 5Digittest@osboxes:~$ ./a.shEnter key: @Something else

Цикл может задаваться тремя разными способами:

Выполняется в интервале указанных значений (либо указанного множества):

for [ <условие> ] do <команды> done

Выполняется, пока соблюдается условие:

while [ <условие> ] do <команды> done

Выполняется, пока не перестанет соблюдаться условие:

until [ <условие> ] do <команды> done

Добавим в скрипт с планетами цикл с условием while и будем выходить из скрипта, если вместо имени планеты будет введено EXIT

#!/bin/bashPLANET="-"while [ $PLANET != "EXIT" ]do  echo -n "Enter the name of planet: "  read PLANET  if [ $PLANET != "EXIT" ]  then.    echo -n "The $PLANET has "    case $PLANET in      Mercury | Venus ) echo -n "no";;      Earth ) echo -n "one";;      Mars ) echo -n "two";;      Jupiter ) echo -n "79";;      *) echo -n "an unknown number of";;    esac  echo " satellite(s)."  fidone

Здесь мы также добавили условие, при котором оператор выбора будет выполняться только в случае, если введено не EXIT. Таким образом, мы будем запрашивать имя планеты и выводить количество её спутников до тех пор, пока не будет введено EXIT:

test@osboxes:~$ ./script4.shEnter the name of planet: EarthThe Earth has one satellite(s).Enter the name of planet: JupiterThe Jupiter has 79 satellite(s).Enter the name of planet: Planet123The Planet123 has an unknown number of satellite(s).Enter the name of planet: EXIT

Нужно отметить, что условие while [ $PLANET != "EXIT" ] можно заменить на until [ $PLANET == "EXIT" ]. == означает "равно", != означает "не равно".

Приведем пример циклов с указанием интервалов и множеств:

#!/bin/bashrm *.datecho -n "File count: "read countfor (( i=1; i<=$count; i++ ))do  head -c ${i}M </dev/urandom >myfile${i}mb.datdonels -l *.datecho -n "Delete file greater than (mb): "read maxsizefor f in *.datdo  size=$(( $(stat -c %s $f) /1024/1024))  if [ $size -gt $maxsize ]  then.    rm $f    echo Deleted file $f  fidonels -l *.datread

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

В первом цикле (for (( i=1; i<=$count; i++ ))) мы генерируем несколько файлов, количество которых задано в переменной count, которую введет пользователь. В команду head передаем количество мегабайт, считываемых из устройства /dev/random, чтение из которого позволяет получать случайные байты.

Символ < указывает перенаправление входного потока (/dev/urandom) для команды head.

Символ > указывает перенаправление выходного потока (вывод команды head -c ${i}M ) в файл, имя которого мы генерируем на основе постоянной строки с добавлением в неё значения переменной цикла (myfile${i}mb.dat).

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

Во втором цикле (for f in *.dat) мы перебираем все файлы .dat в текущей директории и сравниваем размер каждого файла со значением, введенным пользователем. В случае, если размер файла больше, мы удаляем этот файл.

В конце скрипта выводим список файлов .dat, чтобы отобразить список оставшихся файлов (ls -l *.dat). Результаты теста:

test@osboxes:~$ ./script5.shFile count: 10-rw-rw-r-- 1 test test 10485760 Nov  9 08:48 myfile10mb.dat-rw-rw-r-- 1 test test  1048576 Nov  9 08:48 myfile1mb.dat-rw-rw-r-- 1 test test  2097152 Nov  9 08:48 myfile2mb.dat-rw-rw-r-- 1 test test  3145728 Nov  9 08:48 myfile3mb.dat-rw-rw-r-- 1 test test  4194304 Nov  9 08:48 myfile4mb.dat-rw-rw-r-- 1 test test  5242880 Nov  9 08:48 myfile5mb.dat-rw-rw-r-- 1 test test  6291456 Nov  9 08:48 myfile6mb.dat-rw-rw-r-- 1 test test  7340032 Nov  9 08:48 myfile7mb.dat-rw-rw-r-- 1 test test  8388608 Nov  9 08:48 myfile8mb.dat-rw-rw-r-- 1 test test  9437184 Nov  9 08:48 myfile9mb.datDelete file greater than (mb): 5Deleted file myfile10mb.datDeleted file myfile6mb.datDeleted file myfile7mb.datDeleted file myfile8mb.datDeleted file myfile9mb.dat-rw-rw-r-- 1 test test 1048576 Nov  9 08:48 myfile1mb.dat-rw-rw-r-- 1 test test 2097152 Nov  9 08:48 myfile2mb.dat-rw-rw-r-- 1 test test 3145728 Nov  9 08:48 myfile3mb.dat-rw-rw-r-- 1 test test 4194304 Nov  9 08:48 myfile4mb.dat-rw-rw-r-- 1 test test 5242880 Nov  9 08:48 myfile5mb.dat

Мы создали 10 файлов (myfile1mb.dat .. myfile10mb.dat) размером от 1 до 10 мегабайт и далее удалили все файлы .dat размером больше 5 мегабайт. При этом для каждого удаляемого файла вывели сообщение о его удалении (Deleted file myfile10mb.dat). В конце вывели список оставшихся файлов (myfile1mb.dat .. myfile5mb.dat).

В следующей части мы рассмотрим функции, планировщик заданий cron, а также различные полезные команды.

Подробнее..
Категории: *nix , Linux , Unix , Ubuntu , Bash , Bash scripting , Ssh , Virtualbox , Debian , Shells

Основы Bash-скриптинга для непрограммистов. Часть 3

16.02.2021 20:15:11 | Автор: admin

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

Функции

Часто используемые, повторяющиеся блоки имеет смысл выделять в отдельные функции, чтобы при необходимости их запускать, передавая параметры.

Определение функции выглядит следующим образом:

<имя_функции>() {  <команды>  return <число>}funciton <имя_функции>() {  <команды>  return <число>}

Первый вариант ближе к синтаксису языка С и считается более переносимым, во втором варианте круглые скобки можно не указывать. Также может отсутствовать оператор return, если функция не возвращает значения.

Самый простой пример скрипта, содержащего объявление и вызов функции будет выглядеть так:

#!/bin/bashf() {  echo Test}f

Мы объявили функцию f, которая выводит слово Test, и затем вызвали её:

test@osboxes:~$ ./script6.shTest

Так же, как и скрипт, функция может принимать параметры и использовать их, ссылаясь по номеру ($1, $2, , $N). Вызов функции с параметрами в скрипте осуществляется так:

<имя функции> <параметр1> <параметр2> <параметрN>

Функция может возвращать результат своего выполнения (код завершения) в виде числового значения в диапазоне от 0 до 255. Принято считать, что если функция возвращает 0, то она выполнилась успешно, во всех остальных случаях значение содержит код ошибки. Чтобы получить код завершения функции в скрипте, необходимо обратиться к переменной $?. Добавив параметры и возвращаемое значение, получим следующий скрипт:

#!/bin/bashsumm() {  re='^[0-9]+$'  if ! [[ $1 =~ $re ]] ; then    return 1  elif ! [[ $2 =~ $re ]] ; then    return 2  else    s=$(($1 + $2))    return 0  fi}summ $1 $2case $? in 0) echo "The sum is: $s" ;; 1) echo "var1 is not a nubmer" ;; 2) echo "var2 is not a nubmer" ;; *) echo "Unknown error" ;;esac

Здесь мы создали функцию summ, которая принимает 2 параметра и с помощью регулярного выражения ^[0-9]+$ проверяет, является ли каждый из переданных параметров числом. В случае, если первый параметр не число, то код завершения функции будет 1, если второй параметр не число, то код завершения функции будет 2. Во всех остальных случаях функция вычисляет сумму переданных параметров, сохраняя результат в глобальной переменной s.

Скрипт вызывает функцию, передавая её на вход параметры, которые были переданы ему самому при вызове. Далее проверяется код завершения функции и выдается соответствующая ошибка, если код не равен 0, иначе выдается сумма, сохраненная в переменной s. Протестируем скрипт:

test@osboxes.org:~$ ./script7.sh abc 123var1 is not a nubmertest@osboxes.org:~$ ./script7.sh 234 defvar2 is not a nubmertest@osboxes.org:~$ ./script7.sh 10 15The sum is: 25

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

Для того, чтобы объявить переменную локальной, используется слово local, например local loc_var=123. Важно отметить, все что переменные, объявляемые в теле функции, считаются необъявленными до тех пор, пока эта функция не будет вызвана.

Объединим все воедино, создав на основе рассмотренных ранее структур следующий скрипт:

#!/bin/bashclearFiles() {  rm *.dat  if [ $? -eq 0 ]  then    echo Files deleted  fi}genFiles() {  for (( i=1; i<=$1; i++ ))  do    head -c ${i}M </dev/urandom >myfile${i}mb.dat  done  ls -l *.dat}delFiles() {for f in *.dat  do    size=$(( $(stat -c %s $f) /1024/1024 ))    if [ $size -gt $1 ]    then      rm $f      echo Deleted file $f    fi  done  ls -l *.dat}showWeather() {  curl -s "https://weather-broker-cdn.api.bbci.co.uk/en/observation/rss/$1" | grep "<desc" | sed -r 's/<description>//g; s/<\/description>//g'}menu() {  clear  echo 1 - Delete all .dat files  echo 2 - Generate .dat files  echo 3 - Delete big .dat files  echo 4 - List all files  echo 5 - Planet info  echo 6 - Show weather  echo "x/q - Exit"  echo -n "Choose action: "  read -n 1 key  echo}while truedo  case "$key" in    "x" | "q" | "X" | "Q") break ;;    "1")      clearFiles      read -n 1    ;;    "2")      echo -n "File count: "      read count      genFiles $count      read -n 1    ;;    "3")      echo -n "Delete file greater than (mb): "      read maxsize      delFiles $maxsize      read -n 1    ;;    "4")      ls -la      read -n 1    ;;    "5")      ./script4.sh      read -n 1    ;;    "6")      echo -n "Enter city code: " # 524901 498817 5391959      read citycode      showWeather $citycode      read -n 1    ;;  esac  menudone

В данном скрипте мы объявили 5 функций:

  • clearFiles

  • genFiles

  • delFiles

  • showWeather

  • menu

Далее реализован бесконечный цикл с помощью оператора while с условием true, в который вложен оператор выбора в зависимости от нажатой клавиши, а также вызов функции menu для отображения списка доступных действий. Данный скрипт в интерактивном режиме позволяет выполнить следующие действия:

  • Удалить все файлы .dat в текущей директории

  • Создать указанное количество файлов

  • Удалить файлы больше определенного размера

  • Вывести список всех файлов текущей директории

  • Запустить скрипт, выдающий информацию о планетах

  • Отобразить погоду по коду указанного города

Не будем подробно разбирать все строки кода, скажем только, что в скрипте демонстрируется вызов другого скрипта, получение информации из интернет, и её парсинг (выделение нужной информации), команда break для выхода из цикла и ряд других возможностей. Предлагаю желающим самостоятельно протестировать скрипт, посмотреть, какие могут быть ошибки при его работе и при вводе различных значений, какие проверки можно добавить, и что можно улучшить. Приведем результаты тестирования:

test@osboxes.org:~$ ./script8.sh1 - Delete all .dat files2 - Generate .dat files3 - Delete big .dat files4 - List all files5 - Planet info6 - Show weatherx/q - ExitChoose action: 4total 40drwxr-xr-x 2 test test 4096 Feb 16 15:56 .drwxr-xr-x 6 root root 4096 Feb 16 15:54 ..-rw------- 1 test test   42 Feb 16 15:55 .bash_history-rw-r--r-- 1 test test  220 Feb 16 15:54 .bash_logout-rw-r--r-- 1 test test 3771 Feb 16 15:54 .bashrc-rw-r--r-- 1 test test  807 Feb 16 15:54 .profile-rw-r--r-- 1 test test 1654 Feb 16 12:40 input.xml-rwxr-xr-x 1 test test  281 Feb 16 14:02 script4.sh-rwxr-xr-x 1 test test  328 Feb 16 13:40 script7.sh-rwxr-xr-x 1 test test 1410 Feb 16 15:24 script8.sh
1 - Delete all .dat files2 - Generate .dat files3 - Delete big .dat files4 - List all files5 - Planet info6 - Show weatherx/q - ExitChoose action: 2File count: 8-rw-rw-r-- 1 test test 1048576 Feb 16 16:00 myfile1mb.dat-rw-rw-r-- 1 test test 2097152 Feb 16 16:00 myfile2mb.dat-rw-rw-r-- 1 test test 3145728 Feb 16 16:00 myfile3mb.dat-rw-rw-r-- 1 test test 4194304 Feb 16 16:00 myfile4mb.dat-rw-rw-r-- 1 test test 5242880 Feb 16 16:00 myfile5mb.dat-rw-rw-r-- 1 test test 6291456 Feb 16 16:00 myfile6mb.dat-rw-rw-r-- 1 test test 7340032 Feb 16 16:00 myfile7mb.dat-rw-rw-r-- 1 test test 8388608 Feb 16 16:00 myfile8mb.dat
1 - Delete all .dat files2 - Generate .dat files3 - Delete big .dat files4 - List all files5 - Planet info6 - Show weatherx/q - ExitChoose action: 3Delete file greater than (mb): 5Deleted file myfile6mb.datDeleted file myfile7mb.datDeleted file myfile8mb.dat-rw-rw-r-- 1 test test 1048576 Feb 16 16:00 myfile1mb.dat-rw-rw-r-- 1 test test 2097152 Feb 16 16:00 myfile2mb.dat-rw-rw-r-- 1 test test 3145728 Feb 16 16:00 myfile3mb.dat-rw-rw-r-- 1 test test 4194304 Feb 16 16:00 myfile4mb.dat-rw-rw-r-- 1 test test 5242880 Feb 16 16:00 myfile5mb.dat
1 - Delete all .dat files2 - Generate .dat files3 - Delete big .dat files4 - List all files5 - Planet info6 - Show weatherx/q - ExitChoose action: 1Files deleted
1 - Delete all .dat files2 - Generate .dat files3 - Delete big .dat files4 - List all files5 - Planet info6 - Show weatherx/q - ExitChoose action: 5Enter the name of planet: MarsThe Mars has two satellite(s).
1 - Delete all .dat files2 - Generate .dat files3 - Delete big .dat files4 - List all files5 - Planet info6 - Show weatherx/q - ExitChoose action: 6Enter city code: 524901    Latest observations for Moscow from BBC Weather, including weather, temperature and wind information      Temperature: -11C (11F), Wind Direction: Northerly, Wind Speed: 0mph, Humidity: 84%, Pressure: 1018mb, , Visibility: Moderate

Примечание: для тестирования работы с данными из Интернет (пункт 6 в меню выбора скрипта) может потребоваться установка curl, это можно сделать командой sudo apt install curl.

Планировщик заданий cron

В случае, когда есть необходимость периодического запуска скриптов, полезно использовать планировщик cron, он позволяет задать расписание запуска скрипта и не требует присутствия администратора.

Просмотр заданий пользователя выполняется командой crontab l. Для редактирования и создания новых задания используется команда crontab e. Строки для запуска команд планировщика в файле конфигурации cron имеют следующий формат:

m h dom mon dow command parameters

Где m минута, h час, dom день месяца, mon месяц, dow день недели, command команда, parameters список параметров. Наглядно этот формат можно представить так:

Например, для того, чтобы в 10 и 30 минут каждого часа каждый день месяца весь год по будням запускать команду, нужно указать следующее:

10,30 * * * 1-5 command parameter1 parameter2

Более простой пример, каждые 15 минут выполнять команду:

*/15 * * * * command

Создадим скрипт для резервного копирования домашней директории пользователя, который будет создавать новый файл бэкапа при каждом запуске:

#!/bin/bashUSER=`whoami`BACKUP_DIR=/tmp/backup_${USER}BACKUP_FILE=${USER}_$(date +%Y%m%d%M%H%S).tgzmkdir -p $BACKUP_DIRcd /tar -zcf $BACKUP_DIR/$BACKUP_FILE home/$USER

Поставим скрипт на выполнение каждый день в 22:00, выполнив команду crontab -eи добавив с помощью открывшегося редактора строку:

00 22 * * * ./backup_home.sh

Проверить, что задача добавлена в планировщик, можно командой crontab -l:

test@osboxes.org:~$ crontab -l00 22 * * * ./backup_home.sh

В результате каждый день в 22:00 будет создаваться резервная копия домашней директории пользователя (в приведенном примере для демонстрации запуск скрипта выполняется каждую минуту):

test@osboxes.org:~$ cd /tmp/backup_test/test@osboxes:/tmp/backup_test$ lltotal 80drwxrwxr-x  2 test test 4096 Feb 16 16:38 ./drwxrwxrwt 17 root root 4096 Feb 16 16:30 ../-rw-rw-r--  1 test test 4431 Feb 16 16:30 test_20210216301601.tgz-rw-rw-r--  1 test test 4431 Feb 16 16:31 test_20210216311601.tgz-rw-rw-r--  1 test test 4431 Feb 16 16:32 test_20210216321601.tgz-rw-rw-r--  1 test test 4431 Feb 16 16:33 test_20210216331601.tgz-rw-rw-r--  1 test test 4431 Feb 16 16:34 test_20210216341601.tgztest@osboxes:/tmp/backup_test$

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

Список полезных команд

Список встроенных команд интерпретатора: help
Помощь по команде: <команда> --help
Мануал по команде: man <команда>
Версия команды: <команда> --version
Список доступных оболочек: cat /etc/shells
Список пользователей и их оболочек: cat /etc/passwd
Текущая директория: pwd
Список файлов текущей директории: ls -la
Текущий пользователь: id
Переменные среды: set
Версия ОС: cat /etc/os-release
Версия ядра: uname -a
Получить привилегии суперпользователя: sudo su -
Установка программы в Debian: apt install mc
Посмотреть утилизацию(загрузку): top
Свободное место: df -h
Сколько занимает директория: du -ks /var/log
Конфигурация сетевых интерфейсов: ifconfig -a
Объем оперативной памяти: free -m
Информация о блочных устройствах(дисках): lsblk
Информация о процессорах: cat /proc/cpuinfo
Список установленных пакетов: apt list --installed
Список и статус сервисов: service --status-all
Перезапуск сервиса: service apache2 restart
Скачать файл: wget https://www.gnu.org/graphics/gplv3-with-text-136x68.png
Получить веб-страницу по URL: curl https://www.google.com
Показать задания планировщика: crontab -l
Редактировать задания планировщика: crontab -e
Вывести новые сообщения в системном логе: tail -f /var/log/syslog
Подсчитать количество строк в выводе команды: <команда> | wc -l
Изменить права доступа к файлу (разрешить выполнение всем): chmod a+x <файл>
Список процессов: ps -ef
Проверить, запущен ли процесс: ps -ef | grep <процесс>
Перейти в предыдущий каталог: cd -
Завершить процесс (сигнал kill): kill -9
Удаление файла: rm <имя файла>
Удаление директории: rm -rf <имя директории>
Редактировать файл: nano <имя_файла>
Топ 10 процессов по использованию памяти: ps aux | awk '{print $6/1024 " MB\t\t" $11}' | sort -nr | head

Полезные ссылки

Руководство по bash: GNU Bash manual
Расширенное руководство по Bash: Advanced Bash-Scripting Guide
Статья на Википедии: Bash
Описание команд и утилит оболочки bash: SS64
Часто задаваемые вопросы о Debian GNU/Linux: Debian FAQ

Заключение

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

На этом пока все, надеюсь, было интересно!
Какие другие полезные команды вы знаете и используете в работе?
Какие интересные конструкции приходилось использовать?
Какие задачи решали?
Всем удачного скриптинга, делитесь мнениями в комментариях!

Подробнее..
Категории: *nix , Linux , Unix , Ubuntu , Bash , Bash scripting , Ssh , Virtualbox , Debian , Shells

Использование быстрых клавиш в командной строке Linux (BASH)

24.02.2021 10:16:44 | Автор: admin

Эта статья посвящена наиболее часто используемым комбинациям клавиш при работе в командной строке Linux (в основном в командном интерпретаторе bash).

Она точно будет полезна начинающим своё знакомство с Linux и, уверен, пригодится тем, кто уже имеет опыт (не всегда годы практики учат работать быстрее).

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

Я попытался продемонстрировать многие примеры при помощи анимированных gif'ок иногда несколько кадров больше скажут, чем несколько абзацев текста.


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

Общие слова и замечания

Большинство продемонстрированных клавиш стандартны для командной строки Linux, но часть из этих комбинаций специфичны для bash (поэтому и пометил это в заголовке). На текущий момент BASH наиболее распространенный и используемый по умолчанию командный интерпретатор в большинстве Linux-дистрибутивах. В других командных интерпретаторах или, проще говоря, shell'ах (рекомендую попробовать zsh и fish) могут быть небольшие отличия в работе. Также часть комбинаций прописана в настройках по умолчанию (например, в файле /etc/inputrc или в /etc/bashrc), которые тоже могут различаться в разных дистрибутивах. И бывает, что некоторые клавиши могут быть настроены и перехватываться графической оболочкой, в которой запущен командный интерпретатор.

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

Часть демонстрируемых клавиш относятся к настройкам терминала. А часть клавиши из командного интерпретатора BASH, и их можно посмотреть, почитав мануал по bash'у (огромный текст пользуйтесь поиском):

man bash

[00:10:40][00:10:40]

^^^ На приведенном фрагменте из мануала: Запись (C-r) означает Ctrl-r, а M-> означает Alt->.

M - это Meta-клавиша

Из истории: М - это Metа-клавиша, сейчас это клавиша Alt, либо также можно вместо неё использовать Esc. Я чаще в её качестве буду использовать клавишу Alt.

Замечание: Если у кого-то настроено переключение раскладки по комбинации Alt-Shift, то учитывайте, что в комбинациях, перечисленных далее и содержащих Alt-Shift, вам надо будет использовать скорее клавишу Esc-Shift (или поменять комбинацию для переключения раскладки клавиатуры, у меня, например, раскладка переключается по CapsLock).

[00:11:18][00:11:18]

^^^ Здесь показано на примере использования комбинаций для перемещения по словам командной строки: Alt-b(или, что то же самое, Esc-b) и Alt-f(или, что то же самое, Esc-f)

Для демонстраций нажатых клавиш использую утилиту screenkey.

Терминал

Итак, начнем с клавиш из настроек терминала. Их можно посмотреть, выполнив команду:
stty -a

А перенастроить, например, так:
stty intr НоваяКлавиша

Ctrl-c сигнал SIGINT

Первые две комбинации клавиш достаточно важные, и часто недавно перешедшие с Windows на Linux НЕправильно их используют: продолжая, как в DOS, для завершения команд использовать комбинацию Ctrl-z, что неверно. В Linux же для того, чтобы попросить (команде посылается сигнал SIGINT) приложение прервать свою работу, используется Ctrl-c.

[00:14:24][00:14:24]

Ctrl-z сигнал SIGTSTP

А комбинация Ctrl-z используется, чтобы попросить (команде посылается сигнал SIGTSTP) приложение остановить свою работу (не завершая) поставить на паузу. Ну, а разбудить его можно командой fg (или bg).

[00:14:36][00:14:36]

Ctrl-d EOF(окончание ввода данных)

Далее разберем комбинацию Ctrl-d. В выводе stty -a эта комбинация значится как EOF, что означает окончание ввода данных. Для примера покажу, как можно создать текстовый файл с определенным текстом без использования текстового редактора:

[00:15:51][00:15:51]

^^^ Здесь видно, что любой набираемый текст перенаправляется в файл /tmp/File.txt, и нет никакой фразы (типа Горшочек, не вари), которую бы команда cat восприняла как окончание ввода точнее, для этого как раз и надо нажать Ctrl-d.

Также пользуюсь комбинаций Ctrl-d для того, чтобы выйти из консоли (например, после того как переключился командой su под другого пользователя или зайдя по ssh на другую машину) вместо набора команды exit или logout:

[00:17:44][00:17:44]

^^^ В правом терминале отображаю историю команд.

...(а внимательный зритель догадается, какой супер-секретный пароль у root'а на стенде)

Ctrl-v ввод следующего символа

Комбинация Ctrl-v позволяет вставить в командную строку следующий за ней символ, не воспринимая его как спецсимвол. (Параметр lnext в выводе stty -a)

[00:19:19][00:19:19]

^^^ Здесь для примера показываю, как в скрипте выводить строку текста с использованием табуляции (знаю, что можно использовать \t в команде echo: echo -e "\tTEXT", но не у всех утилит есть такая возможность, а подобная необходимость вставить спецсимвол случается).

[00:20:38][00:20:38]

^^^ А в этом примере у меня есть файл New Text Document.txt, созданный в ОС Windows при помощи программы Notepad в директории, которую я затем открыл на доступ по сети и примонтировал в Linux в директорию /mnt. Программа Notepad (в отличии от Notepad++) создает файл в DOS-формате в конце каждой строки использует дополнительный символ Возврат каретки. Терминалы часто по умолчанию этот символ не отображают, но он есть, и поэтому, например, команда 'grep "m$" /mnt/New\ Text\ Document.txt' не выведет строку, заканчивающуюся на букву m. Команда cat с опцией -v отображает этот символ. А для того, чтобы при выводе заменить или удалить это символ, воспользовался командой tr (хотя можно было бы использовать специальную для этого утилиту unix2dos).

Ctrl-l очищает экран

Комбинация Ctrl-l очищает экран.

[00:10:51][00:10:51]

История команд

Работа с историей команд классическая тема обычно из любого начального курса по Linux (по крайней мере, среди тех курсов, которые читаются у нас в Сетевой Академии ЛАНИТ). И многие, кто имеет хотя бы небольшой опыт работы с командной строкой, историей команд пользуются как минимум знают, что она есть, и используют стрелки вверх (отобразить предыдущую команду) и вниз (отобразить следующую после отображаемой команду в истории команд), чтобы выбрать, какую из ранее введенных команд либо снова выполнить, либо подредактировать и запустить отредактированную. Но помимо стрелок еще есть ряд полезных комбинаций клавиш, которые позволяют работать с историей команд, быстрее находить нужные команды.

Ctrl-r Поиск по истории

Комбинация Ctrl-r позволяет искать в истории команд команды, содержащие указанный далее текст.

[00:25:21][00:25:21]

^^^ В этом примере мне понадобилось из истории вытащить команду, содержащую текст su: нажав Ctrl-r и набрав искомый текст su, я увидел самую недавнюю команду, содержащую su; при повторном нажатии Ctrl-r отображается предыдущая команда, содержащая su и т.д. При необходимости изменить команду жму стрелку вправо и правлю текст, а чтобы запустить команду - нажимаю Enter.

PgUp/PgDown Поиск по истории

PgUp отображает предыдущую команду начинающуюся с уже введенного текста, PgDown следующую.

[00:27:35][00:27:35]

^^^ В этом примере перемещаюсь между командами, начинающимися с cat. (Часто также ищу команды, начинающиеся с sudo. Или если мне нужно снова отредактировать какой-то файл, который недавно редактировал: набираю vi, жму несколько раз PgUp, а затем Enter.)

В дистрибутивах где это настроено - в /etc/inputrc есть строки:
"\e[5~":history-search-backward"\e[6~":history-search-forward

Alt-_/Alt-./Alt-- вставка аргументов

Комбинация Alt-_ (выполняется нажатием Alt, Shift, -) вставляет последний аргумент из предыдущих команд. (Аналогично работает комбинация Esc-. или, что то же самое, Alt-.)

[00:28:32][00:28:32]

^^^ В данном примере видно, как повторные нажатия Alt-_ вставляют аргументы от пред-пред--идущих команд.

Комбинация Alt - позволяет указать (порядковый номер с конца), какой аргумент вставить клавишей Alt-_ из предыдущей команды.

[00:30:13][00:30:13]

^^^ В данном примере вставляю в командную строку различные аргументы из предыдущей команды.

Alt-# текущую команду преобразовать в комментарий

Бывает, во время набора очень длинной команды понимаю, что мне нужно что-нибудь посмотреть или дополнительно сделать (например, глянуть, какие файлы есть в определенной директории, прочитать мануал по команде, установить нужный пакет). Что делать с уже набранным текстом? Хотелось бы посмотреть нужную информацию и продолжить набирать команду, а не начинать печатать её сначала. Alt-# (выполняется нажатием Alt, Shift, 3. Также можно использовать Esc-#) преобразует текущую набранную команду в комментарий в истории добавляет символ # в начало строки и добавляет полученную строку в историю команд.

[00:32:03][00:32:03]

Ctrl-o повтор команд из истории

Комбинация Ctrl-o позволяет повторять серию команд из истории. То есть нужно из истории команд стрелками выбрать первую команду из серии и нажать Ctrl-o это выполнит текущую команду и выведет из истории следующую. Дальше можно продолжать нажимать Ctrl-o с тем же эффектом.

[00:33:58][00:33:58]

^^^ В примере я написал три команды: одна увеличивает на 1 переменную, которой соответствует год; вторая выводит переменную-год; третья показывает, сколько дней в феврале в указанном году. Дальше, нажимая Ctrl-o, повторяю эту серию из трех команд много раз (один кадр соответствует трем нажатиям).

Автодополнение

Tab автодополнение (в контексте)

Во многих командных интерпретаторах (и в bash в том числе) используется такая возможность, как автодополнение. Как минимум нужно знать, что по нажатию клавиши Tab дописывается название команды. В bash по умолчанию обычно настроено так, что если имеется только один вариант дополнения, то он дописывается по нажатию Tab (также можно использовать Ctrl-i и Esc-Esc). Когда вариантов дополнения много, то по первому нажатию Tab дописывается только общая часть (если она есть). А по второму нажатию Tab отображается список всех доступных вариантов. Дальше можно набрать еще символов уточнить, какое из дополнений нужно, и снова нажать Tab. То же самое с другими дополнениями: имен файлов, имен переменных.

[00:39:20][00:39:20]

^^^ Здесь, например, смотрю (нажав дважды Tab), что есть несколько команд, начинающихся с if, добавив c и нажав Tab, получаю набранной команду ifconfig.

[00:39:31][00:39:31]

^^^ В этом примере дополняю аргументы команды (здесь имена файлов). Также видно, что в случае, когда вариантов много и все не умещаются в окне терминала, их список отображается утилитой для постраничного просмотра (также при очень большом списке вариантов выдается запрос вида Display all 125 possibilities? (y or n) или, как в этом примере, при малом количестве - --More--.

Дополнения имен пользователей, переменных

Часто, когда дописываются аргументы команд по Tab, дописываются имена файлов. Но стоит также отметить, что, в зависимости от контекста, по Tab дописываются и имена переменных (аргументы, начинающиеся с символа $), имена пользователей (аргументы, начинающиеся с символа ~),

[00:40:36][00:40:36]

^^^ Здесь, чтобы набрать $HISTFILESIZE, вместо 13 символов набрал 8 символов ($ H I Tab F Tab S Tab). Помимо того, что так быстрее, это еще и позволяет допускать меньше ошибок при наборе команд, так как не просто печатаю текст, а выбираю из списка установленных переменных.

[00:41:44][00:41:44]

^^^ Здесь дописываю имена пользователей (фактически пишу адрес домашней директории).

Также bash может дополнять не потому, что набранный текст начинается с определенного символа, а по определенным комбинациям клавиш.

Список того, что может дополнять bash, можно посмотреть командой:

bind -P | grep "complet"
possible-username-completions can be found on "\C-x~".complete-username can be found on "\e~".possible-hostname-completions can be found on "\C-x@".complete-hostname can be found on "\e@".possible-variable-completions can be found on "\C-x$".complete-variable can be found on "\e$".possible-command-completions can be found on "\C-x!".complete-command can be found on "\e!".possible-filename-completions can be found on "\C-x/".complete-filename can be found on "\e/".
[00:43:50][00:43:50]

Так, например, видно, что:

  • Ctrl-x ~ покажет список имен пользователей, начинающихся с набранных символов, а дополнить комбинацией Esc-~;

  • Ctrl-x @ список имен машин (согласно /etc/hosts), начинающихся с набранных символов, а дополнить Esc-@;

  • Ctrl-x $ список имен переменных, заданных в этой сессии (можно их также посмотреть командой set), а дополнить Esc-$;

  • Ctrl-x ! список команд (согласно доступных: $PATH, alias, функций, встроенных команд), а дополнить Esc-!;

  • Ctrl-x / список имен файлов, а дополнить Esc-/.

Alt-* вставить дополнения, Ctrl-x * развернуть шаблон

Esc-* (точнее, Esc Shift 8) или, что, то же самое, Alt-* (точнее, Alt, Shift, 8), вставит все варианты дополнения в командную строку. Аналогично можно развернуть список файлов, переменных, имен пользователей.

В примерах ниже разворачиваю список файлов:

Вариант с Alt-*:

[00:44:55][00:44:55]

Вариант с Esc-*:

[00:46:30][00:46:30] bind -P | grep '*'
insert-completions can be found on "\e*".glob-expand-word can be found on "\C-x*".

Ctrl-x * развернет уже написанный в командной строке шаблон, как в примере ниже:

[00:48:39][00:48:39]

Редактирование

Ctrl-w/u/k вырезать слово/начало/конец строки

Ctrl-w вырезать слово (от текущего положения курсора до ближайшего ранее в строке пробела/табуляции). Вырезанное можно затем вставить комбинацией Ctrl-y.

[00:52:52][00:52:52]

Ctrl-u вырезать начало строки (от текущего положения курсора. Если курсор в конце строки, то вырежет целиком строку). Вырезанное можно затем вставить комбинацией Ctrl-y.

Ctrl-k вырезать конец строки (от текущего положения курсора. Если курсор в начале строки, то вырежет целиком строку). Вырезанное можно затем вставить комбинацией Ctrl-y.

Ctrl-y вставить вырезанное

Ctrl-y вставить вырезанный фрагмент командной строки. (В bash используется свой буфер для хранения вырезанных фрагментов называется kill ring).

Важно: Удобно использовать с Alt-y (позволяет прокручивать варианты вставки из буфера).

Ctrl-x Ctrl-e редактировать в $EDITOR

Нажав комбинацию Ctrl-x Ctrl-e, можно редактировать командную строку в любом внешнем редакторе (по умолчанию часто используется редактор vim; переназначить редактор можно, указав в переменной EDITOR). Часто редакторы имеют больше продвинутых возможностей в редактировании текста. Особенно удобно, если редактор умеет подкрашивать синтаксис команд и имеет различные встроенные инструменты для быстрого поиска и исправления ошибок.

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

[00:53:40][00:53:40]

Ctrl-_ undo

Ctrl-_ (точнее, нужно нажать Ctrl Shift -) или Ctrl-x Ctrl-u отменяет последние правки при редактировании командной строки.

Перемещение

Ctrl-a/e в начало/конец строки

Ctrl-a и Ctrl-e перемещение в начало и конец командной строки соответственно. Можно, конечно, пользоваться клавишами Home и End, но так быстрее при использовании, например, таких клавиш, как вырезание Ctrl-w и вставка Ctrl-y.

[00:52:05][00:52:05]

Alt-b/f и Ctrl-/ предыдущее/следующие слово

Alt-b (и тот же эффект у Ctrl-Left) переход в начало предыдущего слова.

Alt-f (и тот же эффект у Ctrl-Right) переход в конец следующего слова.

[00:50:10][00:50:10]

Настройки

bash

Подробнее значения действия редактирования командной строки bash можно посмотреть в мануал по bash'у. Действия, упомянутые в этой статье (в порядке упоминания):

man bash
  • clear-screen (C-l) Clear the screen, then redraw the current line, leaving the current line at the top of the screen.

  • reverse-search-history (C-r) Search backward starting at the current line and moving `up' through the history as necessary. This is an incremental search.

  • reverse-search-history (C-r) Search backward starting at the current line and moving `up' through the history as necessary. This is an incremental search.

  • history-search-backward Search backward through the history for the string of characters between the start of the current line and the point. This is a non-incremental search.

  • history-search-forward Search forward through the history for the string of characters between the start of the current line and the point. This is a non-incremental search.

  • yank-last-arg (M-., M-_) Insert the last argument to the previous command (the last word of the previous history entry). With a numeric argument, behave exactly like yank-nth-arg. Successive calls to yank-last-arg move back through the history list, inserting the last word (or the word specified by the argument to the first call) of each line in turn. Any numeric argument supplied to these successive calls determines the direction to move through the history. A negative argument switches the direction through the history (back or forward). The history expansion facilities are used to extract the last word, as if the "!$" history expansion had been specified.

  • digit-argument (M-0, M-1, ..., M--) Add this digit to the argument already accumulating, or start a new argument. M-- starts a negative argument.

  • insert-comment (M-#) Without a numeric argument, the value of the readline comment-begin variable is inserted at the beginning of the current line. ... The default value of comment-begin causes this command to make the current line a shell comment.

  • operate-and-get-next (C-o) Accept the current line for execution and fetch the next line relative to the current line from the history for editing. A numeric argument, if supplied, specifies the history entry to use instead of the current line.

  • complete (TAB) Attempt to perform completion on the text before point. Bash attempts completion treating the text as a variable (if the text begins with $), username (if the text begins with ~), hostname (if the text begins with @), or command (including aliases and functions) in turn. If none of these produces a match, filename completion is attempted.

  • complete-username (M-~) Attempt completion on the text before point, treating it as a username.

  • possible-username-completions (C-x ~) List the possible completions of the text before point, treating it as a username.

  • complete-hostname (M-@) Attempt completion on the text before point, treating it as a hostname.

  • possible-hostname-completions (C-x @) List the possible completions of the text before point, treating it as a hostname.

  • complete-variable (M-$) Attempt completion on the text before point, treating it as a shell variable.

  • possible-variable-completions (C-x $) List the possible completions of the text before point, treating it as a shell variable.

  • complete-command (M-!) Attempt completion on the text before point, treating it as a command name. Command completion attempts to match the text against aliases, reserved words, shell functions, shell builtins, and finally executable filenames, in that order.

  • possible-command-completions (C-x !) List the possible completions of the text before point, treating it as a command name.

  • complete-filename (M-/) Attempt filename completion on the text before point.

  • possible-filename-completions (C-x /) List the possible completions of the text before point, treating it as a filename.

  • insert-completions (M-*) Insert all completions of the text before point that would have been generated by possible-completions.

  • glob-expand-word (C-x *) The word before point is treated as a pattern for pathname expansion, and the list of matching filenames is inserted, replacing the word. If a numeric argument is supplied, an asterisk is appended before pathname expansion.

  • unix-word-rubout (C-w) Kill the word behind point, using white space as a word boundary. The killed text is saved on the kill-ring.

  • unix-line-discard (C-u) Kill backward from point to the beginning of the line. The killed text is saved on the kill-ring.

  • kill-line (C-k) Kill the text from point to the end of the line.

  • yank (C-y) Yank the top of the kill ring into the buffer at point.

  • yank-pop (M-y) Rotate the kill ring, and yank the new top. Only works following yank or yank-pop.

  • undo (C-_, C-x C-u) Incremental undo, separately remembered for each line.

  • backward-word (M-b) Move back to the start of the current or previous word. Words are composed of alphanumeric characters (letters and digits).

  • forward-word (M-f) Move forward to the end of the next word. Words are composed of alphanumeric characters (letters and digits).

bind -P

Можно посмотреть, какие клавиши к каким действиям редактирования командной строки bash привязаны для этого можно воспользоваться командой bind -P.

Есть и много других интересных комбинаций для примера можно глянуть:

Клавиши, переключающие регистр букв:
bind -P | egrep "case|capitalize"

[00:58:35][00:58:35]

Клавиши, меняющие слова/буквы местами:
bind -p | grep "transpose"

Также можно настроить свои привязки например, чтобы по комбинации Ctrl-f выводился результат команды date:

bind -x'"\C-f": date'

[01:00:50][01:00:50]

/etc/inputrc (настройки библиотеки readline)

Так как bash и многие shell'ы используют библиотеку readline для взаимодействия с командной строкой, то можно перенастроить соответствия комбинаций клавиш и действий в /etc/inputrc.

Например, такие строки меняют поведение по умолчанию клавиш Вверх и Вниз

... вместо предыдущей/последующей команды они будут включать поиск по истории команд очень удобно, часто этим пользуюсь:

"\e[A": history-search-backward"\e[B": history-search-forward

Коды клавиш можно посмотреть, используя комбинацию Ctrl-v, упомянутую выше в этой статье.

Итог

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

Если, по вашему мнению, стоило упомянуть какие-то еще полезные комбинации - напишите в комментариях.


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

Ну, и приходите к нам учитьсяв Сетевую Академию ЛАНИТ!

P.S. Также рекомендую к прочтению мою предыдущую Habr-статью:
Как устроена графика в Linux: обзор различных сред оформления рабочего стола.


А в завершение конкурс для внимательных зрителей.
Кто первым правильно напишет в комментариях ответ на три вопроса, получит 25% скидку на курс по написанию Bash-скриптов:

  1. дата, когда проходил вебинар Сетевой Академии ЛАНИТ по теме этой статьи;

  2. какой пароль у пользователя root на системе, используемой на вебинаре?

  3. какой дистрибутив Линукс использовался?

Подробнее..

Что такое bash shell

26.03.2021 14:15:31 | Автор: admin

И то, и другое интерпретаторы командной строки в линуксе. То есть если вы откроете командную строку и введете любую команду, да хоть:

cd /home

То именно интерпретатор ее расшифрует и скажет компьютеру он хочет перейти в директорию /home. Компьютер ведь не понимает команды на русском / английском языке. Ему нужны байтики. Этим и занимается интерпретатор переводом с нашего на компьютерный язык.

Так что cd /home это shell-команда! Или bash. Смотря какой интерпретатор установлен в вашей системе. В каждой операционной системе установлен интерпретатор по умолчанию. У них есть какие-то различия, но есть и набор базовых команд, которые понимают все: cd, mv, cp, ls (в винде эти команды немного другие)

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

См также:

Основные linux-команды для новичка что можно выполнять в консоли

Например, создадим скриптик, который создаст директорию и в ней файлик:

mkdir /home/testcd /home/testtouch test.txt

Так, команды записали, осталось сохранить их в файлик. Скрипты хранят в файлах с расширением .sh, поэтому назовем файл first_script.sh. Но есть нюанс линуксу плевать на ваше расширение файла. Его может вообще не быть, и все равно скрипт останется скриптом. Почему? Потому что у любого скрипта в первой строке должен содержаться путь к интерпретатору. Например:

#!/bin/bashили#!/bin/sh

Весь файл целиком:

#!/bin/bashmkdir /home/testcd /home/testtouch test.txt

И даже если у такого файла не будет расширения вовсе, его можно будет запустить как скрипт:

sh first_script# Проверяем директорию /home там появилась папка test с файлом test.txt внутри.

Расширение .sh ставится для понимания челоком. Зашел в директорию:

Ага, что тут у нас? Файлы sh, скрипты какие-то лежат...

Скрипты могут быть простые, а могут быть сложные. Вот, например, в одном проекте мы вначале вручную обновляли тестовые платформы. Для обновления надо:

  1. Остановить сервис.

  2. Переподложить war-файл с приложением (лежат они в директории /opt)

  3. Запустить сервис

Сервиса два, допустим это test и cloud. Так что шагов уже 6.

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

#!/bin/bashservice test stopcp test.war /opt/jboss-test/binservice test startservice cloud stopcp cloud.war /opt/jboss-cloud/binservice cloud start

Собираешь приложение, подкладываешь к скриптику и запускаешь 1 команду вместо 6. Удобно! Это называется автоматизация рутины =)

Другой пример с того же проекта мы делали серверное приложение. И во время установки приложения на сервере linux нужно выполнить пункты по настройке самой системы. Например, увеличить параметр max_map_count сколько максимум памяти может использовать процесс.

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

Вообще, если вы отдаете установку приложения на откуп чужим админам, лучше потом проверять а всё ли настроено верно? Конечно, обычно на production (машина, с которой работают реальные пользователи) настраивают всё внимательно, это на тестовых стендах могут что-то пропустить... Но лучше перебдеть!

Мы написали скрипт по проверке настройки окружения (символ # в начале строки означает, что это комментарий):

#!/bin/sh## check sysctl#if [ -f /proc/sys/vm/max_map_count ] && [ $(cat /proc/sys/vm/max_map_count) -ge 16777216  ]; thenecho "vm.max_map_count: ok"elseecho "vm.max_map_count: failed"fiif [ -f /proc/sys/vm/overcommit_memory ] && [ $(cat /proc/sys/vm/overcommit_memory) -eq 2  ]; thenecho "vm.overcommit_memory: ok"elseecho "vm.overcommit_memory: failed"fiif [ -f /proc/sys/vm/overcommit_ratio ] && [ $(cat /proc/sys/vm/overcommit_ratio) -eq 100  ]; thenecho "vm.overcommit_ratio: ok"elseecho "vm.overcommit_ratio: failed"fiif [ -f /proc/sys/vm/swappiness ] && [ $(cat /proc/sys/vm/swappiness) -le 10  ]; thenecho "vm.swappiness: ok"elseecho "vm.swappiness: failed"fi

В итоге админы настраивают окружение, а потом мы даем им скрипт, просим запустить его и прислать результаты. Я запустила скрипт на голой системе, где, разумеется, параметры настроены не были, и вот ответ:

Видим, что все проверки провалились, статус failed. Если и от админов приходит похожая картина, направляем их в документацию по настройке системы. Если к нам приходят с проблемой падения из-за нехватки памяти, снова просим выполнить скрипт. Так проще локализовать ошибку: это в приложении косяк, или окружение настроено плохо?

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

Когда надо писать скрипт?

  • Когда надо выполнить больше 3 команд за раз проще выполнить одну, запустить скрипт.

  • Когда одну и ту же команду надо выполнять чаще 3 раз лучше автоматизировать эту работу.

По сути своей, bash-скрипты это та же автоматизация. А когда нужна автоматизация? Когда мы хотим избавиться от рутины, от постоянного выполнения одного и того же действия вручную. Повторяете одно и то же каждый день / неделю? Напишите скрипт. Даже если он на 2-3 строчки будет, это правда удобнее. Поверьте, сама делала небольшие скрипты ?

См также по bash:

Википедия

Что такое shell и зачем он нужен

Основы BASH. Часть 1 (Хабр) цикл статей о том, как писать скрипты

См также другие статьи из цикла Что такое...:

Что такое API

Что такое клиент-серверная архитектура

Что такое CI (Continuous Integration)

Что такое транзакция

Что такое XML

Что такое регулярные выражения (regexp)

PS больше полезных статей ищитев моем блоге по метке полезное. А полезные видео намоем youtube-канале

Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru