Несколько советов по использованию bash

Несколько советов по использованию bash

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

1. Потеря комманд в .bash_history

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

bash по умолчанию пишет в историю набранные за сеанс команды только при своем закрытии (перед самоликвидацией). Поэтому при открытии нового терминала в другой вкладке вы не увидите только что набранные команды из первой вкладки — они еще не записаны в хистори.

К тому же закрыв первый терминал, а потом второй вы не найдете в истории команд набранных в первом терминале. Потому что по умолчанию bash не дописывает файл .history, а переписывает.

Исправить ситуацию можно, дописав в конфигурационный файл ~/.bashrc пару строк

    shopt -s histappend
    PROMPT_COMMAND='history -a'

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

2. Эвристическое исправление ошибок директорий

Если дописать такую строчку

    shopt -s cdspell

то bash будет пытаться исправлять допущенные вами опечатки (пропуски и перестановки символов, например /ect/init.d вместо /etc/init.d) в пути у команды cd. Не бойтесь, у rm такая фича работать уже не будет. Только у cd.

3. Не писать в историю подряд идущие строки-дубликаты

Пишем в ~/.bashrc

    export HISTCONTROL="ignoredups"

А если вы не хотите, чтобы в историю попадали вызовы каких-то «неинформативных» команд, то их логирование можно запретить:

    export HISTIGNORE="&:ls:[bf]g:exit"

После этой команды в хистори не будут писаться команды &, ls, bg, fg, exit. Можно дописать и свои, через двоеточие, можно использовать шаблоны.

4. Не разрывать многострочные команды

Еще команда в конфигурационный файл

    shopt -s cmdhist

5. Поиск по истории команд

Иногда команды бывают большими и сложными, и чтобы заново ее не писать и не искать по истории 100 раз нажимая «вверх», можно воспользоваться поиском.

Если вы помните кусочек команды которую хотите найти, то можно просто нажать в bash’e комбинацию Ctrl + R и набрать этот кусочек. bash вам покажет последнюю команду с такой подстрокой. Можно продолжать нажимать Ctrl + R и bash будет выдавать более старые подходящие команды, подходящие под искомую строку.

6. Вернуться в предыдущую директорию

Когда мы работаем в какой-то директории, и нам нужно «выбраться» в другую директорию, что-то там поделать и вернуться назад, можно воспользоваться «cd -«, например

    [toor@localhost html]$ cd /var/www/html
    [toor@localhost html]$ cd /etc/
    [toor@localhost etc]$ vi my.cnf
    [toor@localhost etc]$ cd -
    /var/www/html
    [toor@localhost html]$

Эта команда вернет нас в директорию где мы были раньше.

7. Хранить дату выполнения в истории команд bash

По умолчанию утилита history, не сохраняет в .bash_history время исполнения каждой команды.

В баше трейтьей версии сделать это можно и весьма просто. Если объявить глобальную переменную HISTTIMEFORMAT с форматом выводимых данных, то утилита history будет сохранять и выводить эту дату.

Итак, пишем в ~/.bashrc строчку

export HISTTIMEFORMAT='%h %d %H:%M:%S '

После этого в .bash_history перед каждой командой появится коментарий с цифрой — временем выполнения этой команды в формате timestamp:

#1260787129
htop
#1260802594
export HISTTIMEFORMAT='%h %d %H:%M:%S '
#1260802598
history | grep squid
#1260802658
mc
#1260802777
chown -R svn:svn svn

А командочка history будет выдавать историю данных с датой в формате, который мы переменной задали (в похожем формате выдают дату и время утилита ls):

995  Dec 14 13:38:49 htop
996  Dec 14 17:56:34 export HISTTIMEFORMAT='%h %d %H:%M:%S '
997  Dec 14 17:56:38 history | grep squid
998  Dec 14 17:57:38 mc
999  Dec 14 17:59:37 chown -R svn:svn svn

Но можно сделать и по ГОСТУ, в приятном русскому глазу виде «ДД.ММ.ГГГГ»

export HISTTIMEFORMAT='%d.%m.%Y %H:%M:%S '

А можно и в формате ISO: «YYYY-MM-DD»

export HISTTIMEFORMAT='%Y-%m-%d %H:%M:%S '
By sysadmin on 14.12.2009 | FreeBSD, Linux
Метки: , , , ,

Простая генерация паролей

Достаточно часто на практике приходится генерировать пароли для разных аккаунтов и сервисов — почты, ftp, samba и просто для нерадивых пользователей, теряющих и забывающих пароли при каждом удобном случае. Очевидно, что придумать новый пароль достаточно просто, но его устойчивость к взлому, как правило, будет сомнительной, поскольку любимые наши пароли — 111, 12345 и «интернет», а также год рождения, номер паспорта или мобильного телефона — известны всем и каждому.

Поэтому для себя я использовал простенький скрипт следующего вида (файл passgen.sh):

#!/bin/bash
echo `tr -cd [:digit:] < /dev/urandom | head -c8`

Этому скрипту необходимо дать права на выполенение

chmod u+x passgen.sh

и в дальнейшем использовать из консоли:

~$ ./passgen.sh
93723588

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

#!/bin/bash
x=`tr -cd [:alnum:] < /dev/urandom | head -c8`
echo ${x:0:4}-${x:4:4}

«Расширенная» версия генератора позволит нам получать буквенно-цифровые пароли в формате XXXX-XXXX:

~$ ./genpass.sh
sv81-1AxP
By sysadmin on 18.06.2009 | FreeBSD, Linux
Метки: , , , ,

Прозрачный MC

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

К слову, пользователи KDE могут сделать это штатными следствами – в настройках Konsole, а для пользователей других WM такой способ просто недоступен.

Немного «погуглив», я нашел решение. В любом эдиторе (только не том, что входит в MC), например nano открываем файл ~/.mc/ini (в консоли это выглядит так: nano -w ~/.mc/ini). Идем в самый конец файла и дописываем там следующюю секцию:

[Colors]
base_color=normal=,default:selected=,:marked=,default:markselect=,:menu=,:menuhot=,:menusel=,:menuhotsel=,:dnormal=,:dfocus=,:dhotnormal=,:dhotfocus=,:input=,:reverse=,:executable=,default:directory=,default:link=,default:device=,default:special=,:core=,:helpnormal=,:helplink=,:helpslink=,:

Сохраняем изменения, закрываем, набираем mc и наслаждаемся видом «крутого» MC.

Очень полезное дополнение – MC будет прозрачным только под обычным юзером, от имени которого вы сделали эти изменения, если вы наберете su -c mc, то MC останется непрозрачным (в отличие от «прозрачности для МС» в KDE).

Очень удобно.

By sysadmin on 19.02.2009 | FreeBSD, Linux
Метки: , , ,

Руссификация FreeBSD 7

В /etc/rc.conf добавить:

font8×14=”cp866-8×14″
font8×16=”cp866b-8×16″
font8×8=”cp866-8×8keymap=”ru.koi8-r”
scrnmap=”koi8-r2cp866″

(если нет, то либо используйте sysinstall либо поправьте вручную)

В /etc/ttys для всех теримналов должно быть прописано cons25r вместо cons25, если это не так — исправьте.

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

pw user mod username -L russian

В домашней папке пользователя должен присутствовать файл .inputrc

set meta-flag on
set convert-meta off
set output-meta on
By sysadmin on 11.02.2009 | FreeBSD
Метки: ,

10 трюков в командной строке bash


1. Простой способ перехватить вывод и ошибки

Хотите направить stdout и stderr в один файл?

command &> file

Может вы разбираетесь в некой программе при помощи strace, и желали бы видеть системные вызовы вместе с ошибками программы?

strace badapp &> errors_and_output

Плюсы: легко запоминается, и проще чем «послать ошибки на вывод, а затем всё это в файл».
Совместимость: любой линукс.

2. Распараллеливание циклов

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

for HOST in $(< ListOfHosts); do ssh $HOSTsudo apt-get update& done

Может вам нужна куча ssh-туннелей одновременно:

for HOST in $(< ListOfHosts); do ssh -C -N -R 80:localhost:80 $HOST & done

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

Плюсы: сберегает метрическую туеву хучу (2/3 имперской туевой хучи) времени ожидания завершения.
Совместимость: любой линукс.
Минусы: у баша должны быть ограничения на количество одновременных задач, но пока автор (и переводчик) в них не упёрся.

3. Ловля утечек памяти через крон

Утечки памяти в линуксе нечасты, но бывают, особенно с бета-дистрибутивами или самодельным софтом. Часто выявить программу с подтёком не так-то просто. В линуксе есть программа Out-Of-Memory, позволяющая отыскивать и убивать такие процессы, но пока она сработает, система уже может начать сильно тормозить — настолько, что вы теряете терпение и перезагружаетесь.

Обычный способ узнать потреблении памяти программой это запуск top (или его графического эквивалента, наподобие System Monitor), и проверка Размера Резидентной Части (Res или RSS) интересующих процессов (память, отведённая программой, вам не нужна — утечки происходят от использования, а не от отведения, и программа может отвести (allocate) кучу памяти без вреда для системы). Большинство граждан не в курсе, что top можно запускать пакетно, что означает, что можно использовать cron и top для создания простого отчёта об использовании программой памяти:

  • запустите top
  • кнопками < и > добейтесь сортировки процессов по RSS (размер резидентной части)
  • нажмите W для записи конфигурации в файл
  • добавьте крон-задачу:
  • crontab - <<< '*/15 * * * * top -n 1 -b'

    И каждые 15 минут будете получать письмо с выводом топа.

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

    4. stdin прямо из командной стоки

    Не поняли, что это была за фигня (<<<)? Баш позволяет слать процессам стандартный ввод прямо из командной стоки.

    Плюсы: позволяет писать команды с командной стоки, даже для альтернативно дружественных программ, которые требуют ВСЁ со стандартного ввода. [Грозит кулаком MySQL-ю].
    Совместимость: bash 3 и новее.
    Минусы: всё ещё немало систем с bash 2.

    5. Установить первичный пароль, который надо поменять

    Многие организации имеют хорошие и надёжные политики паролей. Пароли хранятся на виндозных машинах. Линукс либо не не покрывается политикой, либо политика не соблюдается — люди не в курсе авторизации под линукс (большинство граждан не понимают PAM, а линуксовые админы часто не осознают, что линукс может чудесно авторизоваться через Active Directory), и было время, что разработчики OpenSSH не любили PAM (это с тех пор поменялось).

    Поставить пароль, который должен быть поменян при первом логине:

    umask u=rw,go=
    openssl rand -base64 6 | tee -a PasswordFile | passwd –stdin joe
    chage -d 0 joe

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

    Плюсы: пользователи не будут с начальным паролем бесконечно.
    Совместимость: любой линукс с обновлённым OpenSSH (если ваши пользователи заходят в первый раз по SSH). РедХат утверждает, что это всё ещё не работает в RHEL 3/4, но после приложения их обновлений, всё хорошо.
    Минусы: нет.

    6. Простое добавление публичного ключа на удалённый хост

    Для логина но новый хост по ключу надо сначала на этот хост записать публичную часть ключа. Конечно, это можно делать вручную, но вскоре это надоедает (и почему у ssh нет authorized_keys.d…), а ведь для этого есть специальная утилита:

    ssh-copy-id -i .ssh/id_rsa.pub hostname

    Введите пароль последний раз, ssh скажет:

    Now try logging into the machine, with “ssh ‘hostname’”, and check in:

    .ssh/authorized_keys

    to make sure we haven’t added extra keys that you weren’t expecting.

    Попробуйте. До свидания, пароли!

    7. Распаковка RPM без дополнительного софта

    На дебиано-подобных дистрибутивах это не проблема, потому что .deb файлы есть просто .ar архивы. Каждое руководство по РедХату упоминает rpm2cpio (идёт по умолчанию с rpm), но если честно, я не способен запомнить синтаксис cpio, античный формат, сейчас использующийся только, мм, пожалуй, только рпм-ом.

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

    mkdir /tmp/deleteme
     
    rpm -ivh –root /tmp/deleteme –nodeps –noscripts package.rpm


    8. Изменился ли файл с момента поставки

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

    dpkg -S /etc/foo/foo.conf
     
    rpm -qf /etc/foo/foo.conf

    Потом разверните оригинальный пакет при помощи tar (DPKg) или трюка с rpm, данного выше (RPM), и запустите:

    diff /etc/foo/foo.conf /tmp/deleteme/etc/foo/foo.conf

    И найдите разницу.

    Плюсы: быстрое нахождение плохих конфиг-файлов (strace тут тоже может пригодиться)
    Совместимость: любой линукс.
    Минусы: у вас остаётся больше времени на работе, чтобы читать Digg.

    9. — Первым делом отключите связь… Ало? ало? идиоты!

    Ковыряетесь в файрволе удалённо? Нервно как-то, правда? Не то нажал, и связь потеряна.

    Почему бы не откатить ошибку? Зарядите откат того, что вы собираетесь менять.

    at now + 5 minutes <<< 'cp /etc/ssh/sshd_config.old /etc/ssh/sshd_config; service sshd restart'

    Если ошибётесь, процесс выполнится и восстановит установки. А если не ошибётесь, запустите atq, и atrm <номер задачи> для удаления.

    Плюсы: прикрывает задницу на случай ошибки.
    Совместимость: любой линукс, в котором разрешён at, а он обычно да.
    Минусы: помнить, что это надо сделать перед рискованным действием.

    10. Открыт ли порт

    Хотите проверить, запущен ли сетевой сервис? Netcat с опцией -w (сколько ждать) будет полезен:

    nc -w 3 server ssh <<< ' '

    Соединиться на ssh порт на хосте по имени server, ждать 3 секунды перед тем, как послать, мм, ничего, и закрыть соединение. Был ли порт открыт, будет отражено в статусе nc.

    if nc -w 3 localhost 22 <<< ''&> /dev/null
    then
    echo 'Port is open'
    else
    echo 'Port is closed'
    fi

    Источник

    By sysadmin on 10.02.2009 | FreeBSD, Linux
    Метки: , ,

    Фокусы с автодополнением в bash

    После посещения заметки «Создание списка имен хостов для ssh на лету для автодополнения» долго вчитывался в man bash с целью понять, как же там это автодополнение работает. В конце концов, поняв, что с наскоку эту гремучую смесь из фич баша и возможностей readline не осилить, скачал оба исходника и нашел там великое множество примеров использования автозаполнения в bash, значительно упрощающих повседневную работу в консоли. Эти команды можно добавить в ~/.bash_profile или вынести в отдельный файл ~/.bash_completions или /etc/bash_completions и подключать его в .bash_profile. Список текущих дополнений можно посмотреть по команде complete (без параметров).

    Данные скрипты скопированы почти без изменений из папки examples/complete архива с исходными текстами bash. Автор всех скриптов, за исключением последнего — Ian Macdonald. На его странице доступен архив с огромным количеством скриптов автодополнения. Автор последнего скрипта (автодополнение длинных опций у configure) — Manu Rouat. Большое им человеческое спасибо 🙂

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

    complete -d cd mkdir rmdir pushd

    Для команд работы с файлами показываются только файлы (все или определённых типов, в зависимости от команды):

    complete -f cat less more chown ln strip
    complete -f -X '*.gz' gzip
    complete -f -X '*.Z' compress
    complete -f -X '!*.+(Z|gz|tgz|Gz)' gunzip zcat zmore
    complete -f -X '!*.Z' uncompress zmore zcat
    complete -f -X '!*.+(gif|jpg|jpeg|GIF|JPG|bmp)' ee xv
    complete -f -X '!*.+(ps|PS|ps.gz)' gv
    complete -f -X '!*.+(dvi|DVI)' dvips xdvi dviselect dvitype
    complete -f -X '!*.+(pdf|PDF)' acroread xpdf
    complete -f -X '!*.texi*' makeinfo texi2dvi texi2html
    complete -f -X '!*.+(tex|TEX)' tex latex slitex
    complete -f -X '!*.+(mp3|MP3)' mpg123

    Для команд работы с заданиями показываются номера заданий, предваряемые символом «%»:

    complete -A signal kill -P '%'
    complete -A stopped -P '%' bg
    complete -j -P '%' fg jobs disown

    Сетевым командам подставляется список хостов из файла, заданного в переменной окружения HOSTFILE. Обычно это файл /etc/hosts:

    complete -A hostname ssh rsh telnet rlogin ftp ping fping host traceroute nslookup

    По уже упоминавшейся ссылке рекомендуют подставлять команде ssh список хостов из ~/.ssh/known_hosts:

    complete -W "$(echo `cat ~/.ssh/known_hosts | cut -f 1 -d ' ' | sed -e s/,.*//g | uniq | grep -v "["`;)" ssh

    Для ряда встроенных команд bash, а также системных команд тоже свои списки дополнений:

    # подставляются переменные окружения
    complete -v export local readonly unset
    # подставляются параметры команд set, shopt, help, unalias, bind
    complete -A setopt set
    complete -A shopt shopt
    complete -a unalias
    complete -A binding bind

    Для команд управления выполнением программ подставляются названия возможных программ:

    complete -c command time type nohup exec nice eval strace gdb

    Думаю, основной принцип понятен — указываем ключевое слово «complete», за ним некий список возможных «подстав» и далее команды, для которых этот список работает. Список подстановок может быть задан в виде уже готовой опции, как в большинстве вышеприведённых примеров (в фрагменте complete -d cd mkdir rmdir pushd опция -d означает, что должен подставляться список доступных каталогов; на самом деле это сокращённый вид опции -A directory. За списком возможных параметров опции -A обращайтесь к man builtin), в виде списка возможных слов, как в примере с .known_hosts (тогда список предваряется опцией -W), или в виде списка файлов (-X «*.gz» покажет все доступные файлы, которые не совпадают с шаблоном «*gz»; чтобы, наоборот, показать все файлы *.gz, нужно в начало шаблона добавить восклицательный знак).

    Конечно же, этим возможности автодополнения bash не ограничиваются. Можно использовать свои функции, которые сработают после того, как пользователь напишет команду, но перед тем, как будет нажата кнопка Tab, показывающая список возможных параметров. Такие функции должны произвести какие-то действия для нахождения подходящих параметров для автодополнения и заполнить этими параметрами массив COMPREPLY; при этом доступны массив COMP_WORDS, содержащий все слова из уже написанной строки, переменные COMP_CWORD (номер текущего слова в массиве COMP_WORDS), COMP_LINE (вся строка целиком в одной переменной) и еще парочка (см. man bash).

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

    _function_name ()
    {
    ...
    }
    complete -F _function_name command

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

    Для команды umount подставляется список примонтированных файловых систем:

    _umount ()
    {
        local cur
        COMPREPLY=()
        cur=${COMP_WORDS[COMP_CWORD]}
     
        COMPREPLY=( $( mount | cut -d' ' -f 3 | grep ^$cur) )
        return 0
    }
    complete -F _umount umount

    Для команд работы с группами пользователей подставляются названия групп из /etc/group:

    _gid_func ()
    {
        local cur
        COMPREPLY=()
        cur=${COMP_WORDS[COMP_CWORD]}
        COMPREPLY=( $( awk 'BEGIN {FS=":"} {if ($1 ~ /^'$cur'/) print $1}' /etc/group ) )
        return 0
    }
     
    complete -F _gid_func groupdel groupmod

    А теперь те две функции, ради которых, собственно, и затевался этот пост 🙂
    Автодополнение параметров для команды find:

    _find ()
    {
        local cur prev
     
            COMPREPLY=()
            cur=${COMP_WORDS[COMP_CWORD]#-}
            prev=${COMP_WORDS[COMP_CWORD-1]}
     
        case "$prev" in
        -@(max|min)depth)
            COMPREPLY=( $( compgen -W '0 1 2 3 4 5 6 7 8 9' ) )
            return 0
            ;;
        -?(a)newer|-fls|-fprint?(0|f))
            COMPREPLY=( $( compgen -f $cur ) )
            return 0
            ;;
        -fstype)
            # this is highly non-portable (the option to -d is a tab)
            COMPREPLY=( $( cut -d'  ' -f 2 /proc/filesystems | grep ^$cur ) )
            return 0
            ;;
        -gid)
            COMPREPLY=( $( awk 'BEGIN {FS=":"} 
                    {if ($3 ~ /^'$cur'/) print $3}' /etc/group ) )
            return 0
            ;;
        -group)
            COMPREPLY=( $( awk 'BEGIN {FS=":"} 
                    {if ($1 ~ /^'$cur'/) print $1}' /etc/group ) )
            return 0
            ;;
        -?(x)type)
            COMPREPLY=( $( compgen -W 'b c d p f l s' $cur ) )
            return 0
            ;;
        -uid)
            COMPREPLY=( $( awk 'BEGIN {FS=":"} 
                    {if ($3 ~ /^'$cur'/) print $3}' /etc/passwd ) )
            return 0
            ;;
        -user)
            COMPREPLY=( $( compgen -u $cur ) )
            return 0
            ;;
        -[acm]min|-[acm]time|-?(i)?(l)name|-inum|-?(i)path|-?(i)regex| 
        -links|-perm|-size|-used|-exec|-ok|-printf)
            # do nothing, just wait for a parameter to be given
            return 0
            ;;
        esac
     
        # complete using basic options ($cur has had its dash removed here,
        # as otherwise compgen will bomb out with an error, since it thinks
        # the dash is an option to itself)
        COMPREPLY=( $( compgen -W 'daystart depth follow help maxdepth 
                mindepth mount noleaf version xdev amin anewer atime 
                cmin cnewer ctime empty false fstype gid group ilname 
                iname inum ipath iregex links lname mmin mtime name 
                newer nouser nogroup perm regex size true type uid 
                used user xtype exec fls fprint fprint0 fprintf ok 
                print print0 printf prune ls' $cur ) )
     
        # this removes any options from the list of completions that have
        # already been specified somewhere on the command line.
        COMPREPLY=( $( echo "${COMP_WORDS[@]}-" | 
                   (while read -d '-' i; do
                    [ "$i" == "" ] &amp;&amp; continue
                    # flatten array with spaces on either side,
                    # otherwise we cannot grep on word boundaries of
                    # first and last word
                    COMPREPLY=" ${COMPREPLY[@]} "
                    # remove word from list of completions
                    COMPREPLY=( ${COMPREPLY/ ${i%% *} / } )
                    done
                    echo ${COMPREPLY[@]})
              ) )
     
        # put dashes back
        for (( i=0; i &lt; ${#COMPREPLY[@]}; i++ )); do
            COMPREPLY[i]=-${COMPREPLY[i]}
        done
     
        return 0
    }
    complete -F _find find

    Гвоздь программы! Автодополнение параметров для команды configure:

    _longopt_func ()
    {
        case "$2" in
        -*) ;;
        *)  return ;;
        esac
     
        case "$1" in
        ~*)    eval cmd=$1 ;;
        *)  cmd="$1" ;;
        esac
        COMPREPLY=( $("$cmd" --help | sed  -e '/--/!d' -e 's/.*--([^ ]*).*/--1/'| 
    grep ^"$2" |sort -u) )
    }
     
    complete  -o default -F _longopt_func wget bash configure
    By sysadmin on | FreeBSD, Linux
    Метки: , ,

    Частые ошибки программирования на Bash

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

    Большинство имеющихся руководств посвящено тому, как надо писать. Я же расскажу о том, как писать НЕ надо 🙂

    Данный текст является вольным переводом вики-страницы “Bash pitfalls” по состоянию на 13 декабря 2008 года. В силу викиобразности исходника, этот перевод может отличаться от оригинала. Поскольку объем текста слишком велик для публикации целиком, он будет публиковаться частями, по мере перевода.

    1. for i in `ls *.mp3`

    Одна из наиболее часто встречающихся ошибок в bash-сериптах — это циклы типа такого:

    for i in `ls *.mp3`; do     # Неверно!
    some command $i         # Неверно!
    done

    Это не сработает, если в названии одного из файлов присутствуют пробелы, т.к. результат подстановки команды ls *.mp3 подвергается разбиению на слова. Предположим, что у нас в текущей директории есть файл 01 — Don’t Eat the Yellow Snow.mp3. Цикл for пройдётся по каждому слову из названия файла и $i примет значения: «01», «-«, «Don’t», «Eat», «the», «Yellow», «Snow.mp3».

    Заключить всю команду в кавычки тоже не получится:

    for i in "`ls *.mp3`"; do   # Неверно!
    ...

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

    На самом деле использование ls совершенно излишне: это внешняя команда, которая просто не нужна в данном случае. Как же тогда правильно? А вот так:

    for i in *.mp3; do         # Гораздо лучше, но...
    some command "$i"      # ... см. подвох №2
    done

    Предоставьте bash’у самому подставлять имена файлов. Такая подстановка не будет приводить к разделению строки на слова. Каждое имя файла, удовлетворяющее шаблону *.mp3, будет рассматриваться как одно слово, и цикл пройдёт по каждому имени файла по одному разу.

    Дополнительные сведения можно найти в п. 20 Bash FAQ.

    Внимательный читатель должен был заметить кавычки во второй строке вышеприведённого примера. Это плавно подводит нас к подвоху №2.


    2. cp $file $target

    Что не так в этой команде? Вроде бы ничего особенного, если вы абсолютно точно знаете, что в дальнейшем переменные $file и $target не будут содержать пробелов или подстановочных символов.

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

    cp "$file" "$target"

    Без двойных кавычек скрипт выполнит команду cp 01 — Don’t Eat the Yellow Snow.mp3 /mnt/usb, и вы получите массу ошибок типа cp: cannot stat `01′: No such file or directory. Если в значениях переменных $file или $target содержатся символы *, ?, [..] или (..), используемые в шаблонах подстановки имен файлов (”wildmats”), то в случае существования файлов, удовлетворяющих шаблону, значения переменных будут преобразованы в имена этих файлов. Двойные кавычки решают эту проблему, если только «$file» не начинается с дефиса -, в этом случае cp думает, что вы пытаетесь указать ему еще одну опцию командной строки.

    Один из способов обхода — вставить двойной дефис (—) между командой cp и её аргументами. Двойной дефис сообщит cp, что нужно прекратить поиск опций:

    cp -- "$file" "$target"

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

    Ещё один способ — убедиться, что названия файлов всегда начинаются с имени каталога (включая ./ для текущего). Например:

    for i in ./*.mp3; do
    cp "$i" /target
    ...

    Даже если у нас есть файл, название которого начинается с “-”, механизм подстановки шаблонов гарантирует, что переменная содержит нечто вроде ./-foo.mp3, что абсолютно безопасно для использования вместе с cp.


    3. [ $foo = «bar» ]

    В этом примере кавычки расставлены неправильно: в bash нет необходимости заключать строковой литерал в кавычки; но вам обязательно следует закавычить переменную, если вы не уверены, что она не содержит пробелов или знаков подстановки (wildcards).

    Этот код ошибочен по двум причинам:

    1. Если переменная, используемая в условии [, не существует или пуста, строка

    [ $foo = "bar" ]

    будет воспринята как

    [ = "bar" ]

    что вызовет ошибку “unary operator expected”. (Оператор “=” бинарный, а не унарный, поэтому команда [ будет в шоке от такого синтаксиса)
    2. Если переменная содержит пробел внутри себя, она будет разбита на разные слова перед тем, как будет обработана командой [:

    [ multiple words here = "bar" ]

    Даже если лично вам кажется, что это нормально, такой синтаксис является ошибочным.

    Правильно будет так:

    [ "$foo" = bar ]       # уже близко!

    Но этот вариант не будет работать, если $foo начинается с -.

    В bash для решения этой проблемы может быть использовано ключевое слово [[, которое включает в себя и значительно расширяет старую команду test (также известную как [)

    [[ $foo = bar ]]       # правильно!

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

    Возможно, вы видели код типа такого:

    [ x"$foo" = xbar ]    # тоже правильно!

    Хак x»$foo» требуется в коде, который должен работать в древних шеллах, не поддерживающих [[, потому что если $foo начинается с -, команда [ будет дезориентирована.

    Если одна из частей выражения — константа, можно сделать так:

    [ bar = "$foo" ]      # так тоже правильно!

    Команду [ не волнует, что выражение справа от знака “=” начинается с -. Она просто использует это выражение, как строку. Только левая часть требует такого пристального внимания.


    4. cd `dirname “$f”`

    Пока что мы в основном говорим об одном и том же. Точно так же, как и с раскрытием значений переменных, результат подстановки команды подвергается разбиению на слова и раскрытию имен файлов (pathname expansion). Поэтому мы должны заключить команду в кавычки:

    cd "`dirname "$f"`"

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

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

    Такого же эффекта можно достичь, используя более предпочтительный синтаксис $():

    cd "$(dirname "$f")"

    Кавычки внутри $() сгруппированы.


    5. [ «$foo» = bar && «$bar» = foo ]

    Нельзя использовать && внутри «старой» команды test или её эквивалента [. Парсер bash’а видит && снаружи [[ ]] или (( )) и разбивает вашу команду на две, перед и после &&. Лучше используйте один из вариантов:

    [ bar = "$foo" -a foo = "$bar" ]       # Правильно!
    [ bar = "$foo" ] && [ foo = "$bar" ]   # Тоже правильно!
    [[ $foo = bar && $bar = foo ]]         # Тоже правильно!

    Обратите внимание, что мы поменяли местами константу и переменную внутри [ — по причинам, рассмотренным в предыдущем пункте.

    То же самое относится и к ||. Используйте [[, или -o, или две команды [.


    6. [[ $foo > 7 ]]

    Если оператор > используется внутри [[ ]], он рассматривается как оператор сравнения строк, а не чисел. В некоторых случаях это может сработать, а может и не сработать (и это произойдёт как раз тогда, когда вы меньше всего будете этого ожидать). Если > находится внутри [ ], всё ещё хуже: в данном случае это перенаправление вывода из файлового дескриптора с указанным номером. В текущем каталоге появится пустой файл с названием 7, и команда test завершится с успехом, если только переменная $foo не пуста.

    Поэтому операторы > и < для сравнения чисел внутри [ .. ] или [[ .. ]] использовать нельзя.

    Если вы хотите сравнить два числа, используйте (( )):

    ((foo > 7))                             # Правильно!

    Если вы пишете для Bourne Shell (sh), а не для bash, правильным способом является такой:

    [ $foo -gt 7 ]                          # Тоже правильно!

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

    Двойные квадратные скобки также поддерживают такой синтаксис:

    [[ $foo -gt 7 ]]                        # Тоже правильно!


    7. count=0; grep foo bar | while read line; do ((count++)); done; echo “number of lines: $count”

    На первый взгляд этот код выглядит нормально. Но на деле переменная $count останется неизменной после выхода из цикла, к большому удивлению bash-разработчика. Почему так происходит?

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

    В данном случае цикл for является частью конвейера и выполняется в отдельной подоболочке со своей копией переменной $count, инизиализированной значением переменной $count из родительской оболочки: “0?. Когда цикл заканчивается, использованная в цикле копия $count отбрасывается и команда echo показывает неизменённое начальное значение $count (”0?).

    Обойти это можно несколькими способами.

    Можно выполнить цикл в своей подоболочке (слегка кривовато, но так проще и понятней и работает в sh):

    # POSIX compatible
    count=0
    cat /etc/passwd | (
    while read line ; do
    count=$((count+1))
    done
    echo "total number of lines: $count"
    )

    Чтобы полностью избежать создания подоболочки, используйте перенаправление (в Bourne shell (sh) для перенаправления также создаётся subshell, поэтому будьте внимательны, такой трюк сработает только в bash):

    # только для bash!
    count=0
    while read line ; do
    count=$(($count+1))
    done < /etc/passwd
    echo "total number of lines: $count"

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

    while read LINE; do
    echo "-> $LINE"
    done < <(grep PATH /etc/profile)

    Ещё пара интересных способов разрешения проблемы с субшеллами обсуждается в Bash FAQ #24.


    8. if [grep foo myfile]

    Многих смущает практика ставить квадратные скобки после if и у новичков часто создаётся ложное впечатление, что [ является частью условного синтаксиса, так же, как скобки в условных конструкциях языка C.

    Однако такое мнение — ошибка! Открывающая квадратная скобка ([) — это не часть синтаксиса, а команда, являющаяся эквивалентом команды test, лишь за тем исключением, что последним аргументом этой команды должна быть закрывающая скобка ].

    Синтаксис if:

    if COMMANDS
    then
    COMMANDS
    elif COMMANDS     # необязательно
    then
    COMMANDS
    else              # необязательно
    COMMANDS
    fi

    Как видите, в синтаксисе if нет никаких [ или [[!

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

    if выполняет первый набор команд, и в зависимости от кода возврата последней команды из этого набора определяет, будет ли выполнен блок команд из секции «then» или же выполнение скрипта продолжится дальше.

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

    if grep foo myfile > /dev/null; then
    ...
    fi

    Обратите внимание, что мы отбрасываем стандартный вывод grep: нам не нужен результат поиска, мы просто хотим знать, присутствует ли строка в файле. Если grep находит строку, он возвращает 0, и условие выполняется; в противном случае (строка в файле отсутствует) grep возвращает значение, отличное от 0. В GNU grep перенаправление >/dev/null можно заменить опцией -q, которая говорит grep’у, что ничего выводить не нужно.


    9. if [bar=»$foo»]

    Как было объяснено в предыдущем параграфе, [ — это команда. Как и в случае любой другой команды, bash предполагает, что после команды следует пробел, затем первый аргумент, затем снова пробел, и т.д. Поэтому нельзя писать всё подряд без пробелов! Правильно вот так:

    if [ bar = "$foo" ]

    bar, =, «$foo» (после подстановки, но без разделения на слова) и ] являются аргументами команды [, поэтому между каждой парой аргументов обязательно должен присутствовать пробел, чтобы шелл мог определить, где какой аргумент начинается и заканчивается.


    10. if [ [ a = b ] && [ c = d ] ]

    Снова та же ошибка. [ — команда, а не синтаксический элемент между if и условием, и тем более не средство группировки. Вы не можете взять синтаксис C и переделать его в синтаксис bash простой заменой круглых скобок на квадратные.

    Если вы хотите реализовать сложное условие, вот правильный способ:

    if [ a = b ] && [ c = d ]

    Заметьте, что здесь у нас две команды после if, объединённые оператором &&. Этот код эквивалентент такой команде:

    if test a = b && test c = d

    Если первая команда test возвращает значение false (любое ненулевое число), тело условия пропускается. Если она возвращает true, выполняется второе условие; если и оно возвращает true, то выполняется тело условия.


    11. cat file | sed s/foo/bar/ > file

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

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

    sed 's/foo/bar/g' file > tmpfile && mv tmpfile file
    <pre>
    Следующий фрагмент будет работать только при использовании GNU sed 4.x и выше:
    <pre lang="bash" escaped="true">
    sed -i 's/foo/bar/g' file
    <pre>
    Обратите внимание, что при этом тоже создаётся временный файл и затем происходит переименование — просто это делается незаметно.
     
    В *BSD-версии sed необходимо обязательно указывать расширение, добавляемое к запасной копии файла. Если вы уверены в своем скрипте, можно указать нулевое расширение:
    <pre lang="bash" escaped="true">
    sed -i '' 's/foo/bar/g' file

    Также можно воспользоваться perl 5.x, который, возможно, встречается чаще, чем sed 4.x:

    perl -pi -e 's/foo/bar/g' file

    Различные аспекты задачи массовой замены строк в куче файлов обсуждаются в Bash FAQ #21.


    12. echo $foo

    Эта относительно невинно выглядящая команда может привести к неприятным последствиям. Поскольку переменная $foo не заключена в кавычки, она будет не только разделена на слова, но и возможно содержащийся в ней шаблон будет преобразован в имена совпадающих с ним файлов. Из-за этого bash-программисты иногда ошибочно думают, что их переменные содержат неверные значения, тогда как с переменными всё в порядке — это команда echo отображает их согласно логике bash, что приводит к недоразумениям.

    MSG="Please enter a file name of the form *.zip"
    echo $MSG

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

    Please enter a file name of the form freenfss.zip lw35nfss.zip

    Вот ещё пример:

    VAR=*.zip       # VAR содержит звёздочку, точку и слово "zip"
    echo "$VAR"     # выведет *.zip
    echo $VAR       # выведет список файлов, чьи имена заканчиваются на .zip

    На самом деле, команда echo вообще не может быть использована абсолютно безопасно. Если переменная содержит только два символа “-n”, команда echo будет рассматривать их как опцию, а не как данные, которые нужно вывести на печать, и абсолютно ничего не выведет. Единственный надёжный способ напечатать значение переменной — воспользоваться командой printf:

    printf "%sn" "$foo"
    <pre>
     
    </ br></ br><h3>13. $foo=bar</h3>
    Нет, вы не можете создать переменную, поставив “$” в начале её названия. Это не Perl. Достаточно написать:
    <pre lang="bash" escaped="true">
    foo=bar


    14. foo = bar

    Нет, нельзя оставлять пробелы вокруг “=”, присваивая значение переменной. Это не C. Когда вы пишете foo = bar, оболочка разбивает это на три слова, первое из которых, foo, воспринимается как название команды, а оставшиеся два — как её аргументы.

    По этой же причине нижеследующие выражения также неправильны:

    foo= bar    # НЕПРАВИЛЬНО!
    foo =bar    # НЕПРАВИЛЬНО!
    $foo = bar  # АБСОЛЮТНО НЕПРАВИЛЬНО!
     
    foo=bar     # Правильно.


    15. echo <<EOF

    Встроенные документы полезны для внедрения больших блоков текстовых данных в скрипт. Когда интерпретатор встречает подобную конструкцию, он направляет строки вплоть до указанного маркера (в данном случае — EOF) на входной поток команды. К сожалению, echo не принимает данные с STDIN.

    # Неправильно:
    echo <<EOF
    Hello world
    EOF
     
    # Правильно:
    cat <<EOF
    Hello world
    EOF


    16. su -c ’some command’

    В Linux этот синтаксис корректен и не вызовет ошибки. Проблема в том, что в некоторых системах (например, FreeBSD или Solaris) аргумент -c команды su имеет совершенно другое назначение. В частности, в FreeBSD ключ -c указывает класс, ограничения которого применяются при выполнении команды, а аргументы шелла должны указываться после имени целевого пользователя. Если имя пользователя отсутствует, опция -c будет относиться к команде su, а не к новому шеллу. Поэтому рекомендуется всегда указывать имя целевого пользователя, вне зависимости от системы (кто знает, на каких платформах будут выполняться ваши скрипты…):

    su root -c 'some command' # Правильно.


    17. cd /foo; bar

    Если не проверить результат выполнения cd, в случае ошибки команда bar может выполниться не в том каталоге, где предполагал разработчик. Это может привести к катастрофе, если bar содержит что-то вроде rm *.

    Поэтому всегда нужно проверять код возврата команды «cd». Простейший способ:

    cd /foo && bar

    Если за cd следует больше одной команды, можно написать так:

    cd /foo || exit 1
    bar
    baz
    bat ... # Много команд.

    cd сообщит об ошибке смены каталога сообщением в stderr вида bash: cd: /foo: No such file or directory. Если вы хотите вывести своё сообщение об ошибке в stdout, следует использовать группировку команд:

    cd /net || { echo "Can't read /net.  Make sure you've logged in to the Samba network, and try again."; exit 1; }
    do_stuff
    more_stuff

    Обратите внимание на пробел между { и echo, а также на точку с запятой перед закрывающей }.

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

    Кстати, если вы много работаете с директориями в bash-скрипте, перечитайте man bash в местах, относящихся к командам pushd, popd и dirs. Возможно, весь ваш код, напичканный cd и pwd, просто не нужен :).

    Вернёмся к нашим баранам. Сравните этот фрагмент:

    find ... -type d | while read subdir; do
    cd "$subdir" && whatever && ... && cd -
    done

    с этим:

    find ... -type d | while read subdir; do
    (cd "$subdir" && whatever && ...)
    done

    Принудительный вызов подоболочки заставляет cd и последующие команды выполняться в subshell’е; в следующей итерации цикла мы вернёмся в начальное местонахождение вне зависимости от того, успешной ли была смена директории или же она завершилась с ошибкой. Нам не нужно возвращаться вручную.

    Кроме того, предпоследний пример содержит ещё одну ошибку: если одна из команд whatever провалится, мы можем не вернуться обратно в начальный каталог. Чтобы исправить это без использования субшелла, в конце каждой итерации придётся делать что-то вроде cd «$ORIGINAL_DIR», а это добавит ещё немного путаницы в ваши скрипты.


    18. [ bar == «$foo» ]

    Оператор == не является аргументом команды [. Используйте вместо него = или замените [ ключевым словом [[:

    [ bar = "$foo" ] && echo yes
    [[ bar == $foo ]] && echo yes


    19. for i in {1..10}; do ./something &; done

    Нельзя помещать точку с запятой “;” сразу же после &. Просто удалите этот лишний символ:

    for i in {1..10}; do ./something & done

    Символ & сам по себе является признаком конца команды, так же, как “;” и перевод строки. Нельзя ставить их один за другим.


    20. cmd1 && cmd2 || cmd3

    Многие предпочитают использовать && и || в качестве сокращения для if … then … else … fi. В некоторых случаях это абсолютно безопасно:

    [[ -s $errorlog ]] && echo "Uh oh, there were some errors." || echo "Successful."

    Однако в общем случае эта конструкция не может служить полным эквивалентом if … fi, потому что команда cmd2 перед && также может генерировать код возврата, и если этот код не 0, будет выполнена команда, следующая за ||. Простой пример, способный многих привести в состояние ступора:

    i=0
    true && ((i++)) || ((i--))
    echo $i   # выведет 0

    Что здесь произошло? По идее, переменная i должна принять значение 1, но в конце скрипта она содержит 0. То есть последовательно выполняются обе команды i++ и i–. Команда ((i++)) возвращает число, являющееся результатом выполнения выражения в скобках в стиле C. Значение этого выражения — 0 (начальное значение i), но в C выражение с целочисленным значением 0 рассматривается как false. Поэтому выражение ((i++)), где i равно 0, возвращает 1 (false) и выполняется команда ((i–)).

    Этого бы не случилось, если бы мы использовали оператор преинкремента, поскольку в данном случае код возврата ++i — true:

    i=0
    true && (( ++i )) || (( --i ))
    echo $i    # выводит 1

    Но нам всего лишь повезло и наш код работает исключительно по “случайному” стечению обстоятельств. Поэтому нельзя полагаться на x && y || z, если есть малейший шанс, что y вернёт false (последний фрагмент кода будет выполнен с ошибкой, если i будет равно -1 вместо 0)

    Если вам нужна безопасность, или вы сомневаетесь в механизмах, которые заставляют ваш код работать, или вы ничего не поняли в предыдущих абзацах, лучше не ленитесь и пишите if … fi в ваших скриптах:

    i=0
    if true; then
    ((i++))
    else
    ((i--))
    fi
    echo $i   # выведет 1.

    Bourne shell это тоже касается:

    # Выполняются оба блока команд:
    $ true && { echo true; false; } || { echo false; true; }
    true
    false


    21. Касательно UTF-8 и BOM (Byte-Order Mark, метка порядка байтов)

    В общем: в Unix тексты в кодировке UTF-8 не используют метки порядка байтов. Кодировка текста определяется по локали, mime-типу файла, или по каким-то другим метаданным. Хотя наличие BOM не испортит UTF-8 документ в плане его читаемости человеком, могут возникнуть проблемы с автоматической интерпретацией таких файлов в качестве скриптов, исходных кодов, файлов конфигурации и т.д. Файлы, начинающиеся с BOM, должны рассматриваться как чужеродные, так же как и файлы с DOS’овскими переносами строк.

    В шелл-скриптах: «Там, где UTF-8 может прозрачно использоватся в 8-битных окружениях, BOM будет пересекаться с любым протоколом или форматом файлов, предполагающим наличие символов ASCII в начале потока, например, #! в начале шелл-скриптов Unix» ://unicode.org/faq/utf_bom.html#bom5


    22. echo «Hello World!»

    Проблема в том, что в интерактивной оболочке Bash эта команда вызовет ошибку:

    bash: !": event not found

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

    Очевидное решение здесь не работает:

    $ echo "hi!"
    hi!

    Можно заключить эту строку в одинарные кавычки:

    echo 'Hello World!'

    Но самое подходящее решение здесь — временно выключить параметр histexpand. Это можно сделать командой set +H или set +o histexpand:

    set +H
    echo "Hello World!"

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

    mp3info -t "Don't Let It Show" ...
    mp3info -t "Ah! Leah!" ...

    Одинарные кавычки здесь не подходят, поскольку названия песен содержат апострофы в названиях, а использование двойных кавычек приведёт к проблеме с подстановкой истории команд (а если бы в именах файлов ещё и двойные ковычки содержались, получилось бы вообще черте что). Поскольку лично я (Greg Wooledge, автор текста) никогда не использую подстановку истории команд, я просто поместил команду set +H в свой .bashrc. Но это вопрос привычки и каждый решает для себя сам.


    23. for arg in $*

    В Bash’е, так же, как и в других оболочках семейства Bourne shell, есть специальный синтаксис для работы с позиционными параметрами по очереди, но $* и $@ не совсем то, что вам нужно: после подстановки параметров они становятся списком слов, переданных в аргументах, а не списком параметров по отдельности.

    Вот правильный синтаксис:

    for arg in "$@"

    Или просто:

    for arg

    for arg соответствует for arg in «$@». Заключенная в двойные кавычки переменная «$@» — это специальная уличная магия, благодаря которой каждый аргумент командной строки заключается в двойные кавычки, так что он выглядит как отдельное слово. Другими словами, «$@» преобразуется в список «$1» «$2» «$3» и т.д. Этот трюк подойдёт в большинстве случаев.

    Рассмотрим пример:

    #!/bin/bash
    # неправильно
    for x in $*; do
    echo "parameter: '$x'"
    done

    Этот код напечатает:

    $ ./myscript 'arg 1' arg2 arg3
    parameter: 'arg'
    parameter: '1'
    parameter: 'arg2'
    parameter: 'arg3'

    Вот как это должно выглядеть:

    #!/bin/bash
    # правильно!
    for x in "$@"; do
    echo "parameter: '$x'"
    done
     
    $ ./myscript 'arg 1' arg2 arg3
    parameter: 'arg 1'
    parameter: 'arg2'
    parameter: 'arg3'


    24. function foo()

    В некоторых шеллах это работает, но не во всех. Никогда не комбинируйте ключевое слово function со скобками (), определяя функцию.

    Некоторые версии bash позволяют одновременно использовать и function, и (), но ни в одной другой оболочке так делать нельзя. Некоторые интерпретаторы, правда, воспримут function foo, но для максимальной совместимости лучше использовать:

    foo() {
    ...
    }


    25. echo «~»

    Замена тильды (tilde expansion) происходит только когда символ ~ не окружён кавычками. В этом примере echo выведет ~ в stdout, вместо того, чтобы вывести пользовательский домашний каталог.

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

    "~/dir with spaces"       #  "~/dir with spaces"
    ~"/dir with spaces"       # "~/dir with spaces"
    ~/"dir with spaces"       # "/home/my photos/dir with spaces"
    "$HOME/dir with spaces"   # "/home/my photos/dir with spaces"


    26. local varname=$(command)

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

    Поэтому эти команды лучше разделять:

    local varname
    varname=$(command)
    rc=$?
    By sysadmin on | FreeBSD, Linux
    Метки: , ,

    Обработка текстов в *nix

    Очень часто администраторам *nix-систем приходится выполнять различные выборки, сортировки, группировки неструктурированных данных. Благо система имеет уйму утилит для этого. Рассмотрим в примерах выборку определённых полей файла. Надо отобрать из файла данные размещённые в первой, пятой и шестой колонках файла /etc/passwd.

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

       1. cat /etc/passwd | awk -F: '{print $1" "$5" "$6}'
     
          ...
          avahi Avahi mDNS daemon,,, /var/run/avahi-daemon
          haldaemon Hardware abstraction layer,,, /home/haldaemon
          ...
     
       2. cat /etc/passwd | awk -F: '{print $1":"$5":"$6}'
     
          ...
          avahi:Avahi mDNS daemon,,,:/var/run/avahi-daemon
          haldaemon:Hardware abstraction layer,,,:/home/haldaemon
          ...
     
       3. cat /etc/passwd | cut -d":" -f1,5,6
     
          ...
          avahi:Avahi mDNS daemon,,,:/var/run/avahi-daemon
          haldaemon:Hardware abstraction layer,,,:/home/haldaemon
          ...
     
       4. cut -d":" -f1,5,6 /etc/passwd
     
          ...
          avahi:Avahi mDNS daemon,,,:/var/run/avahi-daemon
          haldaemon:Hardware abstraction layer,,,:/home/haldaemon
          ...

    Чем отличаются друг от друга эти варианты?

    Первые два варианта: 1 и 2, — используют cat и awk. Первая утилита выводит содержимое файла /etc/passwd, вторая отбирает необходимые поля. «-F:» указывает на то, что разделителем полей в потоке служит двоеточие. Обратный слэш () перед двоеточием предписывает читать двоеточие, как двоеточие и не пытаться обработать его, как спецсимвол. В общем-то, обратный слэш используется в данном случае для перестраховки. Обратите внимание, что в первом случае мы вывели поля (нумеруются начиная с 1), разделённые пробелом, а во втором — двоеточием. Если вам необходимо модифицировать вывод, используйте эту связку, если вам нужно просто вывести требуемые поля пользуйтесь вариантами три или четыре.

    В третьем варианте используется связка cat и cut. Первый выводит, второй режет 🙂 Эту связку можно можифицировать так, как указано в варианте 4, так как cut может принимать на вход и поток (вариант три), и файл (вариант четыре). -d»:» — указывает считать разделителем полей двоеточие. Здесь разделитель указан в кавычках, поэтому использовать обратный слэш () нет необходимости. -f1,5,6 говорит о том, что нужно отобразить 1-ое, 5-ое и 6-ое поля. Имейте ввиду, что для cut порядок полей, пречисленных после -f, не имеет значения. Поля будут выводится так, как они расположены в файле. Другими словами, вы просто указываете какие поля вас интересуют, а не порядок вывода полей. Если вам необходимо изменить порядок полей, воспользуйтесь awk.Например, так:

    cat /etc/passwd | awk -F: '{print $6" "$5" "$1}'
     
    ...
    /var/run/avahi-daemon Avahi mDNS daemon,,, avahi
    /home/haldaemon Hardware abstraction layer,,, haldaemon
    ...

    Кроме того, awk также умеет получать данные не только из потока, но и напрямую из файла. Т.е. последний пример мы можем переписать так:

    awk -F: '{print $6" "$5" "$1}' /etc/passwd
     
    ...
    /var/run/avahi-daemon Avahi mDNS daemon,,, avahi
    /home/haldaemon Hardware abstraction layer,,, haldaemon
    ...

    Почему я чаще пользуюсь конструкцией «cat | awk» или «cat | cut»? Всё просто. Когда я пытаюсь отобрать поля, я не всегда помню их точное положение в файле. Тогда я делаю: cat /etc/passwd

    Получаю полный список. После чего, уточнив нужные поля, нажимаю стрелочку вверх (в bash это вызов предыдущей команды) и дописываю | awk……..

    By sysadmin on 08.02.2009 | FreeBSD, Linux
    Метки: , , , , , ,

    Поиск файла по его временным атрибутам

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

    Например

    • Найти все папки измененные с конца обеда вчерашнего дня
    $> find . -newerct 'yesterday 14:00' -and -type d -print
    • Найти и удалить все файлы старше двух недель
    $> find . -not -newerBt '2 week ago' -and -type f -unlink
    • Нужно найти все файлы, которые были созданы с трех часов ночи позавчерашнего до обеда вчерашнего дня
    $> find . -newerBt '2 day ago 03:00' -and -not -newerBt 'yesterday 13:00' -and -type f -print

    Список ключей, ответственных за фильтрацию по временным атрибутам

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

    -Bnewer file
    Смотрите описание -newerBm.

    -Btime n[smhdw]
    Если не определены никакие единицы времени, этот ключ вычисляется как истина если разница между временем создания файла и временем начала поиска, округленная до 24-часового периода в большую сторону, составляет n 24-часовых периодов.
    Если определены единицы времени, этот ключ вычисляется как истина если разница между временем последнего изменения информации о файле и временем начала поиска составляет n единиц времени. Пожалуйста обратитесь к описанию ключа -atime для получения информации о поддерживаемых единицах времени.

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

    -anewer file
    Смотрите описание -neweram.

    -atime n[smhdw]
    Если не определены никакие единицы времени, этот ключ вычисляется как истина если разница между временем последнего доступа к файлу и временем начала поиска, округленная до 24-часового периода в большую сторону, составляет n 24-часовых периодов.
    Если определены единицы времени, этот ключ вычисляется как истина если разница между временем последнего доступа к файлу и временем начала поиска составляет точно n единиц времени. Возможные единицы времени:

    • s секунда
    • m минута (60 секунд)
    • h час (60 минут)
    • d день (24 часов)
    • w неделя (7 дней)

    В аргументе ключа единицы времени можно комбинировать в произвольном порядке. Для примера «-atime -1h30m». Единицы времени используются только совместно с модификаторами «+» или «-».

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

    -cnewer file
    Смотрите описание -newercm.

    -ctime n[smhdw]
    Если не определены никакие единицы времени, этот ключ вычисляется как истина если разница между временем последнего изменения информации о файле и временем начала поиска, округленная до 24-часового периода в большую сторону, составляет n 24-часовых периодов.
    Если определены единицы времени, этот ключ вычисляется как истина если разница между временем последнего изменения информации о файле и временем начала поиска составляет n единиц времени. Пожалуйста обратитесь к описанию ключа -atime для получения информации о поддерживаемых единицах времени.

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

    -mtime n[smhdw]
    Если не определены никакие единицы времени, этот ключ вычисляется как истина если разница между временем последнего изменения файла и временем начала поиска, округленная до 24-часового периода в большую сторону, составляет n 24-часовых периодов.
    Если определены единицы времени, этот ключ вычисляется как истина если разница между временем последнего изменения файла и временем начала поиска составляет n единиц времени. Пожалуйста обратитесь к описанию ключа -atime для получения информации о поддерживаемых единицах времени.

    -newer file
    -mnewer file

    Истина если у текущего файла более позднее время изменения чем у указанного файла.

    -newerXY file
    Истина если у текущего файла более позднее время доступа (X=a), время создания (X=B) или время изменения (X=m) чем время доступа (Y=a), время создания (Y=B) или время изменения (Y=m) указанного файла (file). В дополнение, если Y=t то file трактуется как время, заданное в одном из описанных в cvs(1) форматов. Отметьте, что -newermm эквивалентен -newer.

    Выдержка из man для cvs(1)
    Поддерживается множество вариантов форматов для описания даты, в частности ISO и Интернет. В случае, если временная зона в дате не указана явным образом, она трактуется в местной временной зоне. Примеры допустимых форматов:

    • 1 month ago
    • 2 hours ago
    • 400000 seconds ago
    • last year
    • last Monday
    • yesterday
    • a fortnight ago
    • 3/31/92 10:00:07 PST
    • January 23, 1987 10:05pm
    • 22:00 GMT
    By sysadmin on | FreeBSD, Linux
    Метки: , , ,

    Массовое переименование файлов с помощью утилиты find

    Иногда бывает жизненно необходимо переименовать кучу файлов. Например заменить пробелы в названии файлов и директорий на нижний знак подчеркивания.
    Сохраняем приведенный ниже скрипт в файл с именем mass-replace.sh

    #!/bin/sh
     
    # Определяем глубину вложенности директорий
    depth=$((`find "$1" -type d -print | 
          sed -E 's/[^/]+//g' | 
          sort | tail -n 1 | 
          wc -c`+1));
    # Макрос, переименовывающий файлы и директории
    replacer='new="`dirname "{}"`/`basename "{}" | sed "s/ /_/g"`"; if [ "$new" != "{}" ]; then mv "{}" "$new"; fi;';
    # Обходим все уровни вложенности
    while [ $depth -ge 0 ];
    do
        # Находим и заменяем
        find "$1" -depth $depth -and -name '* *' -print -exec sh -c "$replacer" ;
        # Меняем уровень вложенности
        depth=$(($depth-1));
    done;

    Сделаем файл исполняемым

    $ chmod +x mass-replace.sh

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

    $ ./mass-replace.sh ~/Warez/Music
    By sysadmin on | FreeBSD, Linux
    Метки: , , ,