Эта статья также есть на английском.
Однажды вечером, перечитывая Джеффри Фридла, я осознал, что даже несмотря на всем доступную документацию, существует множество приемов заточенных под себя. Все люди слишком разные. И приемы, которые очевидны для одних, могут быть неочевидны для других и выглядеть какой-то магией для третьих. Кстати, несколько подобных моментов я уже описывал здесь.
Командная строка для администратора или пользователя — это не только инструмент, которым можно сделать все, но и инструмент, который кастомизируется под себя любимого бесконечно долго. Недавно пробегал перевод на тему удобных приемов в CLI. Но у меня сложилось впечатление, что сам переводчик мало пользовался советами, из-за чего важные нюансы могли быть упущены.
Под катом — дюжина приемов в командной строке — из личного опыта.
Маленькое отступление — в реале я использую множество приемов, в которых могут случайно встретиться имена реальных серверов или юзеров, что может попасть под NDA, поэтому я не мог копи-пастить и специально переписал и максимально упростил все примеры в статье — и если вам покажется, что какой-то прием в данном контексте совершенно бесполезен — возможно это как раз по этой причине. Но в любом случае делитесь вашими идеями в комментариях!
1. Разбиение строки при помощи variable expansions
Часто используют cut или даже awk, чтобы просто получить значение какого-то одного столбца.
Если нужно взять первые символов в строке, используют ${VARIABLE:0:5}.
Но есть очень мощный инструмент, который позволяет манипулировать строками при помощи #, ##, % и %% (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
Результат выполнения
Разница — в несколько десятков раз!$ ./test.sh
using cut
real 0m0.950s
user 0m0.012s
sys 0m0.232s
using ##
real 0m0.011s
user 0m0.008s
sys 0m0.004s
Конечно, пример выше — слишком искусственный. В реальной жизни мы будем обрабатывать реальный файл. А в этом случае из него нужно читать. И для 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
Результат выполнения
$ ./test.sh
$ ./test.sh
using cut
real 0m0.827s
user 0m0.004s
sys 0m0.208s
using ##
real 0m0.613s
user 0m0.436s
sys 0m0.172s
Без комментариев =)Еще парочка примеров:
$ VAR="myClassName = helloClass"
$ echo ${VAR##*= }
helloClass
$ VAR="Hello my friend (enemy)"
$ TEMP="${VAR##*\(}"
$ echo "${TEMP%\)}"
enemy
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
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
На этом я прощаюсь, и буду благодарен за участие в опросе.
И конечно — жду разумной критики и новых приемов от вас!
P.S. Update:
1. все переменные заключены в кавычки — так надо!
2. `` заменен на $() — так правильно!
3. исправлено несколько опечаток, и даже парочка ошибок.
Всем, кто со мной спорит — несмотря на мою воинственность, я действительно стараюсь услышать и разобраться =), поэтому огромное спасибо+ следующим комментаторам: khim, Tanriol, Himura, bolk, firegurafiku, а также dangerous3, RPG, ALexhha, IRyston, McAaron
Only registered users can participate in poll. Log in, please.
Попалось ли вам что-то новое?
36.25% Удивил прием с ## и %%307
26.09% Забавная штука с read вместо sleep221
10.86% Не знал про openssl92
54.19% Больше половины полезно!459
17.95% Ничто не ново под луной :/152
847 users voted. 255 users abstained.