Оригинал на Хабре

1. Разбиение строки при помощи variable expansions

Часто используют cut или даже awk, чтобы просто получить значение какого-то одного столбца.
Но в простых случаях более чем достаточно просто отрезать у переменной лишнее при помощи #, ##, % и %% (bash variable expansions) — с их помощью можно отрезать ненужное по паттерну. Пример ниже показывает, как из строки «username:homedir:shell» можно получить только третий столбец (shell) при помощи cut или при помощи variable expansions (мы используем маску *: и команду ##, что означает отрезать слева все символы до последнего найденного двоеточия):

$ STRING="username:homedir:shell"


$ echo "$STRING"|cut -d ":" -f 3
shell
$ echo "${STRING##*:}"
shell

Второй вариант не запускает еще один процесс (cut), и вообще не использует пайпы, что должно работать гораздо быстрее. А если подобный скрипт выполнить в bash-подсистеме на windows, где пайпы еле шевелятся, разница в скорости будет огромна.

Давайте посмотрим пример на Ubuntu — крутим нашу команду в цикле 1000 раз

$ cat test.sh
#!/usr/bin/env bash
STRING="Name:Date:Shell"
echo "using cut"
time for A in {1..1000}
do
  cut -d ":" -f 3 > /dev/null <<<"$STRING"
done

echo "using ##"

time for A in {1..1000}
do
 echo "${STRING##*:}" > /dev/null
done

Разница — в несколько десятков раз!

Конечно, пример выше — слишком искусственный. В реальной жизни мы будем обрабатывать реальный файл. А в этом случае из него нужно читать. И для cut мы просто перенаправим файлик /etc/passwd. А в случае использования ##, нам придется создать цикл с использованием read. Итак, кто победит в этом варианте?

$ cat test.sh
#!/usr/bin/env bash
echo "using cut"
time for count in {1..1000}
do
  cut -d ":" -f 7 </etc/passwd > /dev/null
done
echo "using ##"
time for count in {1..1000}
do
  while read
  do
    echo "${REPLY##*:}" > /dev/null
  done </etc/passwd
done

 

2. Автокомплит в bash с использованием TAB

Пакет bash-completion сейчас идет в поставке практически всех дистрибутивов, включить его можно или в /etc/bash.bashrc или /etc/profile.d/bash_completion.sh, но чаще всего он уже включен из коробки. В общем автокомплит по TAB — это один из тех удобных моментов, с которыми новичок знакомится в первую очередь.

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

Алиасам не нужно прописывать PATH, не нужно создавать отдельные исполняемый файл — они комфортно могут лежать в .profile или .bashrc.

В *nix обычно используется lowercase для файлов и каталогов, поэтому мне показалось удобным использовать алиасы с использованием uppercase — тогда автокомплит угадывает вашу мелодию с первой ноты работает буквально с первых букв:

$ alias TAsteriskLog="tail -f /var/log/asteriks.log"
$ alias TMailLog="tail -f /var/log/mail.log"
$ TA[tab]steriksLog
$ TM[tab]ailLog

 

3. Автокомплит в bash с использованием TAB — 2

Для более сложных случаев, часто пишутся скрипты и кладутся например в $HOME/bin.
Но есть же функции.
Им опять же не нужен PATH, и для них тоже работает автокомплит.
Поместим функцию LastLogin в .profile (не забудьте перегрузить .profile):

function LastLogin {
  STRING=$(last | head -n 1 | tr -s " " " ")
  USER=$(echo "$STRING"|cut -d " " -f 1)
  IP=$(echo "$STRING"|cut -d " " -f 3)
  SHELL=$( grep "$USER" /etc/passwd | cut -d ":" -f 7)
  echo "User: $USER, IP: $IP, SHELL=$SHELL"
}

 

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

Затем в консоли:

$ L[tab]astLogin

User: saboteur, IP: 10.0.2.2, SHELL=/bin/bash

 

4. Sensitive data 

Если перед командой поставить пробел, она не попадет в bash history, следовательно если в командной строке нужно ввести пароль открытым текстом, лучше такую команду исключить из истории — на примере ниже, echo «hello 2» в истории не появляется:

$ echo "hello"
hello

$ history 2
 2011  echo "hello"
 2012  history 2

$  echo "my password secretmegakey"
my password secretmegakey

$ history 2
 2011  echo "hello"
 2012  history 2

Поведение с пробелом контролируется следующей опцией:
export HISTCONTROL=ignoreboth 

В скриптах, которые например нужно выложить в git или раскладывать их через ansible, можно сделать external bash file который будет лежать только на целевой машине, с правами 600, добавленный в .gitignore и содержать нужные пароли только для конкретного окружения, а в основной скрипт вызываться через source:

secret.sh
PASSWORD=LOVESEXGOD

myapp.sh
. ~/secret.sh
sqlplus -l user/"$PASSWORD"@database:port/sid @mysqfile.sql

 

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

Например, безопасный ssh не имеет опции, которая позволяет передать ему пароль в командной строке. А небезопасный wget — поддерживает опцию --password, и если на данной машине работают другие пользователи, они могут увидеть в списке процессов все параметры вашего wget, пока он запущен.

И наконец самый правильный вариант — шифровать данные (пароли сертификатов, пароли для доступа к удаленным системам, пароли от SQL) используя openssl и мастер ключ.
openssl поддерживает все необходимые опции, чтобы использовать его для шифрования паролей прямо в скриптах. Пример шифрования и дешифрования:

Файл secret.key хранит только одну строку:

$ echo "secretpassword" > secret.key; chmod 600 secret.key

Шифруем нашу строку алгоритмом aes-256-cbc, c использованием случайной соли:

$ echo "string_to_encrypt" | openssl enc -pass file:secret.key -e -aes-256-cbc -a

U2FsdGVkX194R0GmFKCL/krYCugS655yLhf8aQyKNcUnBs30AE5lHN5MXPjjSFML

 

Такую строку можно смело вставлять в скрипт или в конфигурационный файл какого-либо, который безопасно положить в .git — без secret.key его расшифровать будет сложно.

Перед использованием дешифровать в первоначальный вид можно той же командой, заменив только опцию -e на -d:

$ echo 'U2FsdGVkX194R0GmFKCL/krYCugS655yLhf8aQyKNcUnBs30AE5lHN5MXPjjSFML' | openssl enc -pass file:secret.key -d -aes-256-cbc -a

string_to_encrypt

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

 

5. Просмотр логов и grep

Часто можно использовать что-то вроде

tail -f application.log | grep -i error

Или даже так

tail -f application.log | grep -i -P "(error|warning|failure)"

Но не забывайте про опцию -v, которая выводит наоборот — строки, которые НЕ соответствуют шаблону — это позволяет вывести не только строку с проблемой, но и в случае exception все остальные строки, которые к нему относятся вот таким образом (исключаем все строки, в которых есть info, выводим все остальные):

tail -f application.log | grep -v -i "info"

Дополнительные нюансы:

Не забывайте использовать -P, так как по умолчанию grep использует basic regular expression, а не PCRE, и вы можете столкнуться с тем, что просто так группы не работают.

Не забывайте и про другие популярные опции "--line-buffered", "-i".

Если вы знаете регулярные выражения, то при помощи --only-matching, можно значительно улучшить вывод. Но в принципе это редко используется в случае разового использования. Зато очень рекомендую почитать мануал по grep, если вы пишете алиас/функцию/скрипт для многоразового использования.

 

6. Уменьшение размера лог файлов

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

Поэтому очистку файла следует делать либо так (очистится весь файл):

> application.log

Либо так (файл будет урезан до указанного размера):

truncate --size=1M application.log

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

Поэтому можно делать вот так, сохраняя последние 1000 строк:

echo "$(tail -n 1000 application.log)" > application.log

Спасибо Himura за оптимизацию.

P.S. В данном примере мы не рассматриваем самый правильный способ — когда приложение само следит за своим лог-файлом пользуясь, например, log4j, или своим велосипедом или rotatelogs.

 

7. watch следит за тобой!

 Бывает ситуация, когда ждешь какого-то события. Например, пока подключится пользователь (жмешь who несколько десятков раз), или кто-то скопирует по ftp файл (жмешь ls десятки раз).

 Можно использовать

 watch <команда>

 По умолчанию, команда будет выполняться каждые 2 секунды с очисткой экрана, пока не нажать Ctrl+C. Частоту выполнения можно изменить опцией при запуске.

Очень полезно, когда работаешь на одном сервере

 

8. Последовательности в bash

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

for srv in 1 2 3; do echo "server${srv}";done
server1
server2
server3

 

использовать вот такое:

for srv in server{1..3}; do echo "$srv";done
server1
server2
server3

 А еще можно использовать команду seq, чтобы форматировать вывод. Например выравниваем ширину автоматически:

for srv in $(seq -w 1 10); do echo "server${srv}";done
server01
server02
server03
server04
server05
server06
server07
server08
server09
server10

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

for file in *.txt; do name=$(basename "$file" .txt);mv $name{.txt,.lst}; done

 

А можно еще короче, с использованием %:

for file in *.txt; do mv ${file%.txt}{.txt,.lst}; done

 

Для переименования файлов лучше использовать утилиту «rename»

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

 mkdir -p project/src/{main,test}/{java,resources}

  

Получим

project/
   !--- src/
        |--- main/
        |    |-- java/
        |    !-- resources/
        !--- test/
             |-- java/
             !-- resources/

9. tail, несколько файлов и несколько юзеров

Ранее упоминался multitail, который может следить за несколькими файлами сразу. Но он не поставляется из-под коробки, а права для установки есть не всегда.

Но с этим вполне может справиться и обычный tail:

tail -f /var/logs/*.log

Кстати, вернемся на минуту к алиасам с «tail -f».

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

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

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

alias TFapplog='tail -f --pid=$(cat /opt/app/tmp/app.pid) /opt/app/logs/app.log'

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

 

10. создание файла нужного размера

Часто пользуются dd

dd if=/dev/zero of=out.txt bs=1M count=10

 

Но я рекомендую использовать fallocate:

fallocate -l 10M file.txt

На файловых системах, которые поддерживают аллокацию места (xfs, ext4, Btrfs...), данная команда будет выполнена мгновенно, в отличие от dd.

 

11. xargs

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

Первое — список может просто не влезть в командную строку.

Но мы можем ограничить обработку аргументов через опцию -n:

 

$ # создаем файл из 5 строк

for string in string{1..5}; do echo $string >> file.lst; done

$ cat file.lst

string1
string2
string3
string4
string5

saboteur@ubuntu:~$ cat file.lst | xargs -n 2

string1 string2
string3 string4
string5

Второе — команда может выполняться слишком долго, ибо мы запустили ее выполняться в один поток. А если у нас несколько ядер, то полезно запускать xargs в три потока, каждый будет обрабатывать по 2 аргумента:

cat file | xargs -n 2 -P 3

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

cat file | xargs -n 2 -P $(nproc)

 

12. sleep? while? read!

Вместо sleep или бесконечного цикла while, я пишу read, что позволяет одной командой сделать паузу, которую можно в любой момент прервать:

read -p "Press any key to continue " -n 1

Или добавить таймаут, который также можно в любой момент прервать и продолжить выполнение:

read -p "Press any key to continue (autocontinue in 30 seconds) " -t 30 -n 1

 

Можно усложнить конструкцию до полноценной обработки:

REPLY=""
until [ "$REPLY" = "y" ]; do
  # executing some command
  read "Press 'y' to continue or 'n' to break, any other key to repeat this step" -n 1
  if [ "$REPLY" = 'n' ]; then exit 1; fi
done

NewMixer (c) 2017