[ИУ9] Основы информатики

Лекция 14. Файловая система. Командные интерпретаторы

Файловая система — способ хранения информации в долговременной памяти компьютера (жёсткие диски, флешки, …) и соотвеющее API операционной системы.

API — application programming interface — интерфейс прикладного программирования. Так называют набор функций, посредством которых программист может взаимодействовать с операционной системой.

Файл — поименованная область диска. Каталог, папка — поименованная группа файлов. Родительский каталог для файла или папки — это папка, в которой находится данный файл или папка. Корневой каталог — каталог, у которого нет родительского каталога.

Путь к файлу — способ указания конкретного файла в файловой системе.

Далее мы будем рассматривать файловую систему UNIX-подобных операционных систем.

В отличие от Windows, в UNIX-подобных системах дерево файлов единое, т.е. содержимое отдельных устройств подключается в общее дерево папок как подпапки. (На Windows для каждого устройства выделяется отдельная буква диска.)

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

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

Имя файла в UNIX-подобных операционных системах может содержать любые знаки кроме знака / и знака с кодом \0. Символ с кодом \0 запрещён, т.к. API для UNIX-подобных систем пишется на Си, а в Си этот символ является ограничителем строк. А знак / служит для разделения имён каталогов в пути к файлу.

По соглашению, имя файла может содержать точку, часть имени файла после точки определяет тип файла. Например, example.c — исходный текст на Си, index.html — веб-страница. Эта часть имени файла называется «расширение» или «суффикс».

Абсолютный путь к файлу записывается, начиная со знака /:

/папка/папка/…/имя-файла-или-папки

Относительный путь к файлу не начинается со /:

папка/папка/…/имя-файла-или-папки

UNIX-подобная ОС — операционная система, реализующая стандарт POSIX. Примеры: Linux, macOS. На Windows имитируют окружение POSIX такие проекты как Cygwin и MinGW/MSys. В Windows 10 появилась подсистема WSL (Windows Subsystem for Linux), которая также имитирует окружение POSIX.

В путях можно использовать такие синонимы, как . и ... Знак . является синонимом текущей папки, знак .. — родительской папки. Можно считать, что в каждой папке находится папка ., которая является синонимом для неё же самой и папка .., которая является синонимом для родительской папки (ссылка на родительскую папку).

В корневой папке .. ссылается на неё же саму.

Т.е., например, следующие пути будут эквиваленты:

/usr/bin/gcc
/usr/./././bin/./gcc
/var/log/../../usr/bin/gcc
/../../../../usr/bin/gcc

Путь /var/log/.. ссылается на папку /var, т.к. .. в /var/log ссылаются на родительскую для неё папку. /var/log/../.. ссылается на корень.

Ссылки . и .., как правило, используются в относительных путях.

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

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

Исторически в UNIX первой оболочкой была оболочка, которая так и называлась, shell и располагалась по пути /bin/sh. Позже Борн создал оболочку Born Shell, затем был создан open source клон этой оболочки Born Again Shell — bash. Bash располагается по пути /bin/bash. Unix shell стандартизирован в POSIX.

Bash является расширением Unix shell, на большинстве дистрибутивов Linux /bin/sh является ссылкой на /bin/bash.

Bash отображает приглашение командной строки, как правило, включающее имя пользователя, имя компьютера, путь к текущей папке и знак привилегий: $ для ограниченного пользователя, # для администратора (суперпользователя).

Знак ~ является сокращением для домашнего каталога пользователя, каталога вида /home/username.

В командной строке можно вводить как встроенные команды Bash, так и имена программ. Если имя программы не включает знак /, то исполнимый файл программы ищется в стандартных путях поиска, как правило, включающих /bin и /usr/bin. Для суперпользователя — также /sbin и /usr/sbin.

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

./progname

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

./progname arg1 arg2 ...

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

Программа example.c:

#include <stdio.h>

int main(int argc, char *argv[]) {
  printf("program arguments:\n");

  for (int i = 0; i < argc; ++i) {
    printf("[%d] = %s\n", i, argv[i]);
  }

  return 0;
}

Пример работы:

mazdaywik@Mazdaywik-NB10:~$ vim example.c
mazdaywik@Mazdaywik-NB10:~$ gcc example.c
mazdaywik@Mazdaywik-NB10:~$ ./a.out
program arguments:
[0] = ./a.out
mazdaywik@Mazdaywik-NB10:~$ ./a.out hello world
program arguments:
[0] = ./a.out
[1] = hello
[2] = world
mazdaywik@Mazdaywik-NB10:~$

Программы vim (текстовый редактор) и gcc (компилятор Си) получали в качестве аргумента имя файла, программа a.out (результат трансляции) — произвольные строки.

Список стандартных команд оболочки (встроенные команды и стандартные утилиты из /bin):

Для аргументов командной строки существует соглашение, что параметры делятся на ключи и имена файлов. Ключи (опции) всегда начинаются на один или два знака -. Если аргумент не начинается с дефиса — он считается именем файла.

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

Например, команда

mkdir -p foo/bar/baz

создаст папки foo, foo/bar и foo/bar/baz, если их до этого не существовало. Без ключа -p программа выдаст ошибку, т.к. для папки baz родительской папки foo/bar не существует.

У большинства команд (программ) есть ключ -h или --help, который отображает краткую справку. Не для все команд есть справка, выдаваемая через man.

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

В шаблоне знак * означает произвольную последовательность знаков, ? — один знак.

Примеры: *.c — все файлы текущей папки с расширением .c, backups/2020-12-*.zip — архивы, датированные декабрём этого года из папки backups. Если в папке присутствуют файлы с расширениями .cpp и .cxx, то шаблон *.c?? выберет их все.

Пример раскрытия шаблона

mazdaywik@Mazdaywik-NB10:~$ ./a.out *.c*
program arguments:
[0] = ./a.out
[1] = example.c
[2] = hello.cpp

В текущей папке было только 2 файла, подпадающие под шаблон.

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

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

mazdaywik@Mazdaywik-NB10:~$ X=Foo
mazdaywik@Mazdaywik-NB10:~$ echo $X
Foo
mazdaywik@Mazdaywik-NB10:~$ ./a.out "Hello, $X"
program arguments:
[0] = ./a.out
[1] = Hello, Foo
mazdaywik@Mazdaywik-NB10:~$ ./a.out 'Hello, $X'
program arguments:
[0] = ./a.out
[1] = Hello, $X
mazdaywik@Mazdaywik-NB10:~$ ./a.out Hello, $X
program arguments:
[0] = ./a.out
[1] = Hello,
[2] = Foo
mazdaywik@Mazdaywik-NB10:~$ ./a.out '*.c*'
program arguments:
[0] = ./a.out
[1] = *.c*

«Философия Unix гласит:

Дуг Макилрой, изобретатель каналов Unix и один из основателей традиции Unix

Процесс — это экземпляр работающей программы.

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

$ ./program args &

Знак & означает, что процесс запущен в фоне. Список фоновых программ, запущенных в текущем сеансе, можно получить при помощи команды jobs, она выведет пронумерованные процессы. Команда fg переводит фоновый процесс на передний план.

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

Команда fg к приостановленному процессу его возобновляет и переводит на передний план. Команда bg — возобновляет и отправляет в фон.

Пример. Запустили архиватор, увидели, что он будет работать долго, решили послать его в фон:

mazdaywik@Mazdaywik-NB10:~$ tar czf archive.tar.gz *
^Z
[1]+  Остановлен    tar czf archive.tar.gz *
mazdaywik@Mazdaywik-NB10:~$ jobs
[1]+  Остановлен    tar czf archive.tar.gz *
mazdaywik@Mazdaywik-NB10:~$ bg
[1]+ tar czf archive.tar.gz * &
mazdaywik@Mazdaywik-NB10:~$ jobs
[1]+  Запущен          tar czf archive.tar.gz * &
mazdaywik@Mazdaywik-NB10:~$ fg
tar czf archive.tar.gz *
^Z
[1]+  Остановлен    tar czf archive.tar.gz *
mazdaywik@Mazdaywik-NB10:~$ tar czf second-archive.tar.gz * &
[2] 25
mazdaywik@Mazdaywik-NB10:~$ jobs
[1]+  Остановлен       tar czf archive.tar.gz *
[2]-  Запущен          tar czf second-archive.tar.gz * &
mazdaywik@Mazdaywik-NB10:~$ bg 1
[1]+ tar czf archive.tar.gz * &
mazdaywik@Mazdaywik-NB10:~$ jobs
[1]-  Запущен          tar czf archive.tar.gz * &
[2]+  Запущен          tar czf second-archive.tar.gz * &
mazdaywik@Mazdaywik-NB10:~$

Для прерывания процесса используется комбинация клавиш CTRL-C:

mazdaywik@Mazdaywik-NB10:~$ fg 1
tar czf archive.tar.gz *
^C
mazdaywik@Mazdaywik-NB10:~$ fg 2
tar czf second-archive.tar.gz *
^C
mazdaywik@Mazdaywik-NB10:~$ jobs
mazdaywik@Mazdaywik-NB10:~$

Процессы в unix-подобных системах идентифицируются по PID — целое число.

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

Процессам можно посылать сигналы. Для посылки сигналов используется команда kill. Синтаксис

kill [-N] pid

где -N — номер сигнала. По умолчанию посылается сигнал SIGTERM. Сигнал SIGTERM — просьба процессу завершиться. Аналогичную роль играет SIGINT, он как раз посылается с клавиатуры комбинацией клавиш CTRL-C. Сигнал SIGSTOP посылается как CTRL-Z.

Список доступных сигналов с номерами:

kill -l

Сигнал SIGKILL — сигнал на безусловное прерывание программы, имеет код 9. Поэтому, чтобы жёстко убить процесс, нужно набрать

kill -9 pid

Если в программе произошла ошибка доступа к памяти (например, из-за неверного указателя), операционная система посылает процессу сигнал SIGSEGV (segmentation violation, segmentation fault, ошибка сегментации).

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

Но есть 3 по умолчанию открытых дескриптора, которые соответствуют двум устройствам:

Для того, чтобы ввести «конец файла» с клавиатуры, используется комбинация клавиш CTRL-D. На Windows «конец файла» вводится как CTRL-Z.

В языке Си тип FILE* — обёртка над дескрипторами ОС, обёртки над этими тремя дескрипторами доступны как константы stdin, stdout и stderr.

fprintf(stdout, "Hello!\n");

эквивалентно

printf("Hello!\n");

Оболочка bash может перенаправлять дескрипторы. Для запущенной программы можно связать stdin, stdout и stderr с файлом или каналом.

Канал (pipe) — особый тип файла. Если один процесс откроет канал для чтения, а второй — для записи, то всё, что запишет второй, будет читать первый. Когда пишущий процесс канал закроет, читающий увидит «конец файла».

Перенаправление стандартного ввода

$ ./program args... < input.txt

Если исходно программа запрашивала у пользователя ввод с клавиатуры, то теперь она читает файл input.txt.

Перенаправление стандартного вывода:

$ ./program args... > output.txt

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

Пример:

$ echo hello > hello.txt
$ echo world >> hello.txt

Перенаправление стандартного потока ошибок:

$ ./program args... 2> errors.txt

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

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

$ cat header.txt body.txt footer.txt > document.txt

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

$ prog1 args... | prog2 args... | prog3 args

В этом случае stdout каждой из программ будет связан со стандартным вводом (stdin) следующей программы.

Задача: найти в файлах с расширением .c все строки, содержащие #include и вывести их в алфавитном порядке и без повторяющихся строк:

$ cat *.c | grep "#include" | sort | uniq

Многие утилиты в unix-подобных ОС или принимают список файлов в качестве аргументов, или, если файлов не указано, читают стандартный ввод.

Написание скриптов

Исполнимые файлы в UNIX-подобных ОС отличаются от обычных флагом исполнимости. У каждого файла есть три набора флагов rwxrwxrwx, r — доступ на чтение, w — доступ на запись, x — доступ на исполнение. Первая группа — права владельца файла, вторая — права группы пользователей, владеющих файлом, третья — права для всех остальных.

Права доступа типичного файла: rw-r–r–, т.е. владелец может в файл писать, все остальные — только читать.

Права доступа: –x–x–x — файл нельзя прочитать, но можно запустить.

Установка и сброс атрибутов выполняется командой chmod:

chmod +x prog         # добавить флаг исполнимости
chmod +w file.dat     # разрешить запись
chmod -w file.dat     # запретить запись
chmod go-r file.dat   # запретить чтение (r) группе (g) и всем остальным (o)

Исполнимые файлы могут быть либо двоичными в формате исполнимых файлов ОС (ELF для Linux, формат Mach-O для macOS), либо скриптами (сценариями). Скрипты должны начинаться со строки с указанием интерпретатора (так называемый shebang).

#!/путь/до/интерпретатора

Для Bash это

#!/bin/bash

или

#!/bin/sh

Если shebang не указан, то на Linux по умолчанию вызывается /bin/sh.

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

Процессы при завершении устанавливают код возврата. В языке Си кодом возврата является возвращаемым значением функции main():

int main() {
  return 100;
}

По соглашению, успешное завершение работы соответствует коду 0, неуспешное — ненулевому числу, при этом разные значения соответствуют разным ошибкам.

Несколько команд можно объединять знаками (, ), &&, ||.

prog1 && prog2

Код возврата будет нулевым, если обе программы завершились успешно. Если prog1 завершилась неуспешно, prog2 даже не запустится.

./gen-source > source.c && gcc source.c
prog1 || prog2

Соответственно, логическое ИЛИ. prog2 вызовется, если prog1 завершилась неуспешно.

./get-info > info.txt || rm info.txt

Команды в Bash разделяются или переводом строки, или знаком ;.

Оператор ветвления имеет вид:

if команда аргументы... ; then
  команда
  ...
elif команда аргументы... ; then
  команда
  ...
else
  команда
  ...
fi

Код после then выполняется, если команда в условии завершилась успешно.

grep возвращает успех, если что-то нашлось, иначе — неуспех.

if grep ERROR file.txt > /dev/null; then
  echo Были ошибки
else
  echo Ошибок не было
fi

Встроенные команды true и false они всегда завершаются, соответственно, успешно и неуспешно.

Цикл while

while команда аргументы...; do
  команда
  ...
done

Цикл for:

for перем in строка...; do
  команда $перем
  ...
done

Переменные окружения. В UNIX есть понятие переменных окружения — набора некоторых глобальных переменных, которые хранят некоторые строки.

Например, переменная PATH хранит список стандартных путей, в которых ищутся исполнимые файлы. Типичное содержимое: /bin:/usr/bin:/usr/local/bin (пути разделяются двоеточием). HOME — путь к домашнему каталогу пользователя, USER — имя пользователя.

В Bash можно устанавливать переменные среды при помощи синтаксиса

VAR=VALUE

Получить значение переменной можно при помощи $VARNAME или ${VARNAME}.

MY_NAME="Vasiliy Pupkin"
echo $MY_NAME

Можно писать так:

RESULT=false

if …; then
  …
  RESULT=true
  …
fi

if $RESULT; then
  …
fi

Особые переменные среды:

Команда shift (встроенная в Bash) сдвигает аргументы командной строки: $2 становится $1, $3$2 и т.д., значение $1 теряется.

Программы-фильтры — это программы, которые принимают какой-то текст на stdin, фильтруют его как-то и выводят на stdout. Либо, если указаны файлы в командной строке, они читают каждый файл последовательно.

Программы-фильтры как правило используются в конвейерах.

Команда test (она же [)

Команда test позволяет проверить некоторое условие, относящееся к файлам или значениям. Если условие верное, код возврата нулевой, иначе — ненулевой.

Может быть вызвана как test условие или как [ условие ]. Примеры:

[ -e filename ]    # проверяет, существует ли файл
[ 100 -lt 200 ]    # проверяет, что 100 меньше 200
[ "ab" != "cd" ]   # проверяет, что строка "ab" не равно "cd"
[ 100 -ne 200 ]    # числа не равны

[ -e filename.txt ] && [ 100 -ge 100 ]
[ -e filename.txt -a 100 -ge 100 ]
test -e filename.txt -a 100 -ge 100

Ключи команды test см. в man test (для самостоятельного изучения).

Но есть и особый синтаксис. В Bash есть встроенная команда [[, которая по поведению эквивалентна test, но обрабатывается самим Bash.

В Bash можно объявлять функции. Синтаксис:

funcname() {
  тело функции
}

Функция вызывается как обычная команда, параметры функции доступны в её теле как $1, $2, ….

Если команду записать внутри обратных кавычек или внутри скобок $(…), то весь вывод программы на stdout превратится в поледовательность аргументов.

echo `cat file.txt`
echo $(cat file.txt)

Этот синтаксис часто используют при написании функций. Функция возвращает результат на stdout, её вызывают обратными кавычками или $(…) и получают её вывод как строку.

Bash может вычислять арифметические выражения: $((2 + 2 * 2)) → 6.

Встроенная команда read VARNAME считывает из стандартного ввода одну строчку и кладёт её в переменную VARNAME. Если достигнут конец файла, программа завершается неуспешно. Использование:

... | while read X; do
  ...
  ...
done

Пример. Рекурсивный обход папок:

#!/bin/bash

rec() {
  if [ -d "$1" ]; then
    ls "$1" | while read name; do
      rec "$1/$name"
    done
  else
    echo File "$1"
  fi
}

rec "$1"

Для того, чтобы запустить интерпретатор скриптового языка, доступный в PATH, но при этом располагающемся по неизвестному пути, в начало файла добавляют /usr/bin/env:

#!/usr/bin/env python

# дальше код какой-то на Python
...
#!/usr/bin/env node

// Дальше код на JavaScript
...

Подходы к написанию скриптов на интерпретируемых языках программирования.