Обычно, когда люди упоминают «язык сценариев оболочки», на ум приходят bash, ksh, sh или другие подобные языки сценариев Linux/Unix. Язык сценариев — это ещё один способ общения с компьютером. При наличии графического оконного интерфейса (неважно, Windows или Linux), пользователь может перемещать мышь и щёлкать по различным объектам, таким как кнопки, списки, флажки и т. д. Но этот способ очень неудобен каждый раз, когда пользователь хочет, чтобы компьютер/сервер выполнил одну и ту же задачу (например, пакетное преобразование фотографий или загрузку новых фильмов, MP3 и т. д.). Чтобы сделать всё это простым и автоматизированным, мы можем использовать shell-скрипты.
Некоторые языки программирования, такие как Pascal, FoxPro, C, Java и т. д., нужно компилировать перед выполнением. Им требуется подходящий компилятор, чтобы наш код мог выполнить определённую задачу.

Другие языки программирования, такие как PHP, JavaScript, Visual Basic и т. д., не нуждаются в компиляторе; вместо этого им требуется интерпретатор, и мы можем запускать программы без компиляции кода.
Shell-скрипты также похожи на интерпретаторы, но обычно они используются для вызова внешних скомпилированных программ. Затем они захватывают результаты вывода, коды завершения и обрабатывают их в зависимости от ситуации.
Один из самых популярных языков сценариев оболочки в мире Linux — bash. И я думаю (это моё собственное мнение), причина в том, что по умолчанию оболочка bash позволяет пользователям удобно перемещаться по истории команд (ранее выполненных). Напротив, ksh требует некоторых настроек .profile или запоминания некоторых «магических» сочетаний клавиш для просмотра истории и исправления команд.
Хорошо, я думаю, этого введения достаточно. Что касается того, какая среда подходит вам больше всего, я оставлю это на ваше усмотрение. С этого момента я буду говорить только о bash и его скриптах. В следующих примерах я буду использовать CentOS 6.6 и bash-4.1.2. Пожалуйста, убедитесь, что у вас такая же версия или выше.
Потоки shell-скриптов
Язык shell-скриптов похож на беседу с несколькими людьми. Вам просто нужно представить все команды как людей, которые могут помочь вам что-то сделать, если только вы попросите их правильным образом. Например, вы хотите написать документ. Сначала вам нужна бумага. Затем нужно кому-то сообщить содержимое, чтобы он написал его за вас. Наконец, вы хотите где-то его сохранить. Или, скажем, вы хотите построить дом, поэтому вам нужно нанять нужных людей, чтобы расчистить участок. После того как они скажут «работа сделана», другие инженеры смогут помочь вам возвести стены. Наконец, когда эти инженеры тоже скажут «работа сделана», вы можете вызвать маляров, чтобы украсить дом. Что произойдёт, если попросить маляров заняться отделкой до того, как построены стены? Думаю, они начнут жаловаться. Почти все эти человекоподобные команды будут говорить. Если они завершат работу без каких-либо проблем, они скажут «стандартный вывод». Если они не смогут сделать то, о чём вы попросили, — они скажут «стандартная ошибка». Итак, в конечном счёте, все команды слушают вас через «стандартный ввод».
Быстрый пример — когда вы открываете терминал Linux и вводите какой-то текст, вы разговариваете с bash через «стандартный ввод». Итак, давайте спросим у оболочки bashwho am i (Кто я?)затем.
root@localhost ~]# who am i <--- 你通过标准输入对 bash shell 说 root pts/0 2015-04-22 20:17 (192.168.1.123) <--- bash shell通过标准输出回答你
Теперь давайте спросим у bash то, чего он не понимает:
[root@localhost ~]# blablabla <--- 哈,你又在和标准输入说话了 -bash: blablabla: command not found <--- bash通过标准错误在发牢骚了
Первое слово перед ':' обычно является командой, которая жалуется вам. На самом деле, каждый из этих потоков имеет свой индексный номер (примечание переводчика LCTT: номер файлового дескриптора):
- Стандартный ввод (stdin) – 0
- Стандартный вывод (stdout) – 1
- Стандартная ошибка (stderr) – 2
Если вы действительно хотите узнать, что сказал вывод команды, вам нужно перенаправить эту речь в файл (используя знак больше ">" и индекс потока после команды):
[root@localhost ~]# blablabla 1> output.txt -bash: blablabla: command not found
В этом примере мы пытаемся перенаправить поток 1 (stdout) в файл с именем output.txt. Давайте посмотрим, что было сделано с содержимым файла; это можно сделать с помощью команды cat:
[root@localhost ~]# cat output.txt [root@localhost ~]#
Похоже, он пуст. Что ж, теперь давайте перенаправим поток 2 (stderr):
[root@localhost ~]# blablabla 2> error.txt [root@localhost ~]#
Итак, мы видим, что жалобы исчезли. Давайте проверим этот файл:
[root@localhost ~]# cat error.txt -bash: blablabla: command not found [root@localhost ~]#
И действительно! Мы видим, что все жалобы были записаны в файл errors.txt.
Иногда команда выводит одновременноstdoutиstderr. Чтобы перенаправить их в разные файлы, можно использовать следующую конструкцию:
command 1>out.txt 2>err.txt
Чтобы немного сократить выражение, можно опустить "1", потому что по умолчаниюstdoutбудет перенаправлен:
command >out.txt 2>err.txt
Итак, давайте попробуем сделать что-то "плохое". Давайте используем команду rm, чтобы удалить file1 и folder1:
[root@localhost ~]# rm -vf folder1 file1 > out.txt 2>err.txt
Теперь давайте проверим следующие выходные файлы:
[root@localhost ~]# cat out.txt removed `file1' [root@localhost ~]# cat err.txt rm: cannot remove `folder1': Is a directory [root@localhost ~]#
Как мы видим, разные потоки были разделены на разные файлы. Иногда это не очень удобно, потому что мы хотим видеть, что происходило последовательно до или после определённой операции при возникновении ошибки. Чтобы этого добиться, мы можем перенаправить оба потока в один и тот же файл:
command >>out_err.txt 2>>out_err.txt
Примечание: обратите внимание, что я использовал ">>" вместо ">". Это позволяет нам добавлять данные в файл, а не перезаписывать его.
Мы также можем перенаправить один поток в другой:
command >out_err.txt 2>&1
Позвольте объяснить. Стандартный вывод всех команд будет перенаправлен в out_err.txt, вывод ошибок будет перенаправлен в поток 1 (как объяснялось выше), а этот поток будет перенаправлен в тот же файл. Рассмотрим этот пример:
[root@localhost ~]# rm -fv folder2 file2 >out_err.txt 2>&1 [root@localhost ~]# cat out_err.txt rm: cannot remove `folder2': Is a directory removed `file2' [root@localhost ~]#
Глядя на объединённый вывод, можно описать это так: сначалаrmкоманда пытается удалить folder2, но у неё это не получится, потому что Linux требует-rключ, чтобы позволитьrmкоманде удалить папку, тогда как второй file2 будет удалён. Предоставивrmпредоставив-v(verbose) ключ, мы заставляем команду rm сообщать нам о каждом удалённом файле или папке.
Это почти всё, что нужно знать о перенаправлении. Я говорю «почти», потому что есть ещё один важный инструмент перенаправления, который называется «pipe» (конвейер). Используя символ | (pipe), мы обычно перенаправляем потокstdout.
Например, у нас есть текстовый файл такого вида:
[root@localhost ~]# cat text_file.txt This line does not contain H e l l o word This lilne contains Hello This also containd Hello This one no due to HELLO all capital Hello bash world!
И нам нужно найти строки, содержащие "Hello". В Linux есть командаgrep, которая может выполнить эту задачу:
[root@localhost ~]# grep Hello text_file.txt This lilne contains Hello This also containd Hello Hello bash world! [root@localhost ~]#
Когда у нас есть файл и мы хотим выполнить поиск в нём, это хорошо работает. Но что, если нужно найти что-то в выводе другой команды? Да, конечно, мы можем перенаправить вывод в файл, а затем искать в файле:
[root@localhost ~]# fdisk -l>fdisk.out [root@localhost ~]# grep "Disk /dev" fdisk.out Disk /dev/sda: 8589 MB, 8589934592 bytes Disk /dev/mapper/VolGroup-lv_root: 7205 MB, 7205814272 bytes Disk /dev/mapper/VolGroup-lv_swap: 855 MB, 855638016 bytes [root@localhost ~]#
Что, если вы собираетесь искать с помощью grep что-то, заключённое в двойные кавычки и содержащее пробелы!
Примечание: команда fdisk отображает информацию о дисковых накопителях в операционных системах Linux.
Как видим, этот метод очень неудобен, потому что мы скоро засоряем пространство временных файлов. Чтобы выполнить эту задачу, можно использовать конвейеры. Они позволяют перенаправлять потокstdoutодной команды в потокstdinдругой команды:
[root@localhost ~]# fdisk -l | grep "Disk /dev" Disk /dev/sda: 8589 MB, 8589934592 bytes Disk /dev/mapper/VolGroup-lv_root: 7205 MB, 7205814272 bytes Disk /dev/mapper/VolGroup-lv_swap: 855 MB, 855638016 bytes [root@localhost ~]#
Как видите, мы получаем тот же результат без необходимости в каких-либо временных файлах. Мы перенаправляемfdisk stdoutвgrep stdin。
Примечание: перенаправление через конвейер всегда идёт слева направо.
Есть несколько других типов перенаправления, но мы рассмотрим их позже.
Отображение пользовательской информации в оболочке
Как мы знаем, общение с оболочкой и внутри оболочки обычно происходит в разговорной форме. Итак, давайте создадим несколько настоящих скриптов, которые также будут с нами разговаривать. Это научит вас нескольким простым командам и даст лучшее понимание концепции скриптов.
Предположим, мы — менеджер службы поддержки компании, и мы хотим создать shell-скрипт для регистрации информации о звонках: номера телефона, имени пользователя и краткого описания проблемы. Мы планируем хранить эту информацию в обычном текстовом файле data.txt для последующей статистики. Сам скрипт работает в диалоговом режиме, что немного облегчит жизнь сотрудникам службы поддержки. Итак, сначала нам нужно выводить приглашения. Для вывода информации можно использовать команды echo и printf. Обе используются для вывода информации, но printf мощнее, потому что с его помощью можно красиво форматировать вывод; мы можем сделать выравнивание по правому краю, по левому краю или оставить специальное место для информации. Давайте начнём с простого примера. Чтобы создать файл, используйте свой обычный текстовый редактор (kate, nano, vi, ...) и создайте файл с именем note.sh, а затем запишите в него эти команды:
echo "Phone number ?"
Как запустить/выполнить скрипт?
После сохранения файла мы можем запустить его с помощью команды bash, передав наш файл в качестве аргумента:
[root@localhost ~]# bash note.sh Phone number ?
На самом деле, выполнять скрипт таким способом довольно неудобно. Если не использоватьbashв качестве префикса команды, запускать было бы удобнее. Чтобы сделать скрипт исполняемым, можно использовать командуchmod:
[root@localhost ~]# ls -la note.sh -rw-r--r--. 1 root root 22 Apr 23 20:52 note.sh [root@localhost ~]# chmod +x note.sh [root@localhost ~]# ls -la note.sh -rwxr-xr-x. 1 root root 22 Apr 23 20:52 note.sh [root@localhost ~]#

Примечание: Команда ls отображает файлы в текущей папке. Добавив ключ -la, она показывает больше информации о файлах.
Как видим, до выполнения командыchmodскрипт имеет только разрешения на чтение (r) и запись (w). После выполненияchmod +xон получает разрешение на выполнение (x). (Более подробно о разрешениях я расскажу в следующей статье.) Теперь мы просто запускаем его так:
[root@localhost ~]# ./note.sh Phone number ?
Перед именем скрипта я добавил комбинацию ./. . (точка) в мире Unix означает текущее расположение (текущую папку), а / (косая черта) — разделитель папок. (В системах Windows для той же функции мы используем обратную косую черту \ ) Так что вся эта комбинация означает: "Выполнить скрипт note.sh из текущей папки". Думаю, было бы понятнее, если бы я запустил этот скрипт с полным путём:
[root@localhost ~]# /root/note.sh Phone number ? [root@localhost ~]#
Это тоже работает.
Если бы у всех пользователей Linux была одна и та же оболочка по умолчанию, всё было бы хорошо. Если мы просто выполним скрипт, для разбора содержимого скрипта и запуска команд будет использоваться оболочка пользователя по умолчанию. Разные оболочки имеют небольшие различия в синтаксисе, встроенных командах и так далее. Поэтому, чтобы гарантировать, что наш скрипт используетbash, мы должны добавить#!/bin/bashв первую строку файла. Таким образом, оболочка пользователя по умолчанию вызовет/bin/bash, и только после этого будут выполнены команды в скрипте:
[root@localhost ~]# cat note.sh #!/bin/bash echo "Phone number ?"
Только теперь мы на 100% увереныbash, что он будет использоваться для разбора содержимого нашего скрипта. Продолжим.
Чтение ввода
После отображения сообщения скрипт ждёт ответа пользователя. Для получения ответа пользователя используется командаread:
#!/bin/bash echo "Phone number ?" read phone
После выполнения скрипт ждёт ввода пользователя, пока пользователь не нажмёт [ENTER], чтобы завершить ввод:
[root@localhost ~]# ./note.sh Phone number ? 12345 <--- 这儿是我输入的内容 [root@localhost ~]#
Всё, что вы введёте, будет сохранено в переменнойphone; чтобы отобразить значение переменной, мы также можем использовать командуecho:
[root@localhost ~]# cat note.sh #!/bin/bash echo "Phone number ?" read phone echo "You have entered $phone as a phone number" [root@localhost ~]# ./note.sh Phone number ? 123456 You have entered 123456 as a phone number [root@localhost ~]#
В оболочкеbashобычно мы используем знак$(доллара), чтобы указать, что это переменная, за исключением случая чтения в переменную и нескольких других редких случаев, когда мы не используем этот $ (будет объяснено позже).
Хорошо, теперь мы готовы добавить остальные вопросы:
#!/bin/bash echo "Phone number?" read phone echo "Name?" read name echo "Issue?" read issue [root@localhost ~]# ./note.sh Phone number? 123 Name? Jim Issue? script is not working. [root@localhost ~]#
Использование перенаправления потоков
Отлично! Осталось перенаправить всё в файл data.txt. В качестве разделителя полей мы будем использовать символ / (слэш).
Примечание: Вы можете выбрать любой разделитель, который считаете лучшим, но убедитесь, что содержимое файла не содержит этих символов, иначе это приведёт к появлению лишних полей в текстовых строках.
Не забудьте использовать ">>" вместо ">", потому что мы хотим добавить вывод в конец файла!
[root@localhost ~]# tail -2 note.sh read issue echo "$phone/$name/$issue">>data.txt [root@localhost ~]# ./note.sh Phone number? 987 Name? Jimmy Issue? Keybord issue. [root@localhost ~]# cat data.txt 987/Jimmy/Keybord issue. [root@localhost ~]#
Примечание : : команда tailотображает последниеnстрок.
Готово. Давайте запустим её снова, чтобы посмотреть:
[root@localhost ~]# ./note.sh Phone number? 556 Name? Janine Issue? Mouse was broken. [root@localhost ~]# cat data.txt 987/Jimmy/Keybord issue. 556/Janine/Mouse was broken. [root@localhost ~]#
Наш файл растёт. Давайте добавим дату в начало каждой строки. Это пригодится, когда позже мы будем возиться с этой статистикой. Чтобы этого добиться, мы можем использовать команду date и указать определённый формат, потому что мне не нравится формат по умолчанию:
[root@localhost ~]# date Thu Apr 23 21:33:14 EEST 2015 <---- date命令的默认输出 [root@localhost ~]# date "+%Y.%m.%d %H:%M:%S" 2015.04.23 21:33:18 <---- 格式化后的输出
Есть несколько способов прочитать вывод команды в переменную; в этом простом случае мы будем использовать ` (обратный апостроф, не одинарная кавычка, на той же клавише, что и тильда ~):
[root@localhost ~]# cat note.sh #!/bin/bash now=`date "+%Y.%m.%d %H:%M:%S"` echo "Phone number?" read phone echo "Name?" read name echo "Issue?" read issue echo "$now/$phone/$name/$issue">>data.txt [root@localhost ~]# ./note.sh Phone number? 123 Name? Jim Issue? Script hanging. [root@localhost ~]# cat data.txt 2015.04.23 21:38:56/123/Jim/Script hanging. [root@localhost ~]#
Хм... наш скрипт выглядит немного уродливо. Давайте его приукрасим. Если вы хотите вручную прочитать командуread, вы обнаружите, что команда read также может отображать некоторую информацию. Чтобы этого добиться, нам следует использовать опцию -p и сообщение:
[root@localhost ~]# cat note.sh #!/bin/bash now=`date "+%Y.%m.%d %H:%M:%S"` read -p "Phone number: " phone read -p "Name: " name read -p "Issue: " issue echo "$now/$phone/$name/$issue">>data.txt
Прямо из консоли можно найти много интересной информации о различных командах, просто введите:man read, man echo, man date, man ……
Согласны? Так гораздо удобнее!
[root@localhost ~]# ./note.sh Phone number: 321 Name: Susane Issue: Mouse was stolen [root@localhost ~]# cat data.txt 2015.04.23 21:38:56/123/Jim/Script hanging. 2015.04.23 21:43:50/321/Susane/Mouse was stolen [root@localhost ~]#
Курсор находится после сообщения (не на новой строке), что интересно. (Примечание переводчика LCTT: Если использовать команду echo для вывода, можно применить опцию -n, чтобы избежать перехода на новую строку.)
Циклы
Пора улучшить наш скрипт. Если пользователь весь день отвечает на телефонные звонки, не будет ли утомительно запускать его каждый раз? Давайте зациклим эти действия бесконечно:
[root@localhost ~]# cat note.sh
#!/bin/bash
while true
do
read -p "Phone number: " phone
now=`date "+%Y.%m.%d %H:%M:%S"`
read -p "Name: " name
read -p "Issue: " issue
echo "$now/$phone/$name/$issue">>data.txt
done
Я поменял местамиread phoneиnow=dateстроки. Это потому, что я хочу получить время после ввода номера телефона. Если я поставлю это в первую строку цикла, то после одного цикла переменная now получит время сразу после того, как данные сохранены в файл. И это плохо, потому что следующий вызов может быть через 20 минут или даже позже.
[root@localhost ~]# ./note.sh Phone number: 123 Name: Jim Issue: Script still not works. Phone number: 777 Name: Daniel Issue: I broke my monitor Phone number: ^C [root@localhost ~]# cat data.txt 2015.04.23 21:38:56/123/Jim/Script hanging. 2015.04.23 21:43:50/321/Susane/Mouse was stolen 2015.04.23 21:47:55/123/Jim/Script still not works. 2015.04.23 21:48:16/777/Daniel/I broke my monitor [root@localhost ~]#
Примечание: чтобы выйти из бесконечного цикла, можно нажать [Ctrl]+[C]. Оболочка отобразит ^ для обозначения клавиши CTRL.。
Использование перенаправления через канал
Давайте добавим больше возможностей нашему "Франкенштейну". Я хочу, чтобы скрипт выводил некоторую статистику после каждого вызова. Например, я хочу видеть, сколько раз каждый номер мне звонил. Для этого нам нужно выполнить cat для файла data.txt:
[root@localhost ~]# cat data.txt 2015.04.23 21:38:56/123/Jim/Script hanging. 2015.04.23 21:43:50/321/Susane/Mouse was stolen 2015.04.23 21:47:55/123/Jim/Script still not works. 2015.04.23 21:48:16/777/Daniel/I broke my monitor 2015.04.23 22:02:14/123/Jimmy/New script also not working!!! [root@localhost ~]#
Теперь мы можем перенаправить весь вывод вcutкоманду, позволивcutразрезать каждую строку на части (мы используем разделитель "/"), затем вывести второе поле:
[root@localhost ~]# cat data.txt | cut -d"/" -f2 123 321 123 777 123 [root@localhost ~]#
Теперь мы можем перенаправить этот вывод в другую командуsort:
[root@localhost ~]# cat data.txt | cut -d"/" -f2|sort 123 123 123 321 777 [root@localhost ~]#
Тогда останутся только уникальные строки. Чтобы подсчитать уникальные записи, просто добавьте-cопцию кuniqкоманде:
[root@localhost ~]# cat data.txt | cut -d"/" -f2 | sort | uniq -c
3 123
1 321
1 777
[root@localhost ~]#
Просто добавьте это в конец нашего цикла:
#!/bin/bash
while true
do
read -p "Phone number: " phone
now=`date "+%Y.%m.%d %H:%M:%S"`
read -p "Name: " name
read -p "Issue: " issue
echo "$now/$phone/$name/$issue">>data.txt
echo "===== We got calls from ====="
cat data.txt | cut -d"/" -f2 | sort | uniq -c
echo "--------------------------------"
done
Запустите:
[root@localhost ~]# ./note.sh
Phone number: 454
Name: Malini
Issue: Windows license expired.
===== We got calls from =====
3 123
1 321
1 454
1 777
--------------------------------
Phone number: ^C

Текущий сценарий проходит через несколько известных шагов:
- Отобразить сообщение
- Получение пользовательского ввода
- Сохранить значение в файл
- Обработать сохранённые данные
Но если пользователь хоть немного ответственен, ему иногда нужно вводить данные, иногда нужна статистика, а может, он хочет что-то найти в сохранённых данных? Для таких вещей нам нужно использовать switch/case и знать, как красиво форматировать вывод. Это полезно при «рисовании» таблиц в оболочке.
Источник: http://linux.cn/article-5591-1.html