Передать скрипт интерпретатору awk можно двумя способами. По умолчанию скрипт просто указывается в качестве первого аргумента командной строки:
$ awk 'script' input-file…
Но скрипт также может находиться в файле. Например, следующий awk-скрипт выводит длину самой длинной входящей строки:
$ cat longest_line.awk
#!/usr/bin/awk
length > max { max = length; }
END { print max; }
Попытаемся запустить этот скрипт с помощью следующего кода на языке С:
execl("longest_line.awk", "longest_line.awk", "input.txt", (char *) NULL);
Этот вызов execl(), в свою очередь, запускает функцию execve(), которая выполняет команду awk со следующими аргументами:
/usr/bin/awk longest_line.awk input.txt
Данный вызов execve() завершается неудачей, поскольку awk интерпретирует строку longest_line.awk как скрипт, содержащий некорректную команду. Нам нужно как-то сообщить awk о том, что этот аргумент является именем файла со скриптом. Для этого можно воспользоваться параметром — f, указав его в качестве опционального аргумента в начальной строке скрипта. Так awk сможет понять, что в следующем аргументе находится скриптовый файл:
#!/usr/bin/awk — f
length > max { max = length; }
END { print max; }
Теперь наш вызов execl() генерирует следующий список аргументов:
/usr/bin/awk — f longest_line.awk input.txt
Это приводит к успешному запуску интерпретатора awk, который обрабатывает файл input.txt.
Обычно отсутствие в скрипте начальной строки (начинающейся с #!) приводит к неудачному завершению функций exec(). Однако вызовы execlp() и execvp() ведут себя немного иначе. Как вы помните, с помощью переменной среды PATH они получают список каталогов, в которых ищется исполняемый файл. Если найденный файл действительно является исполняемым, но состоит не из бинарного кода и первая строка не начинается с символов #! эти вызовы обращаются к командной оболочке, чтобы та интерпретировала файл. В контексте Linux это означает, что такие файлы обрабатываются так, как будто начинаются с #!/bin/sh.
Все файловые дескрипторы, открываемые программой, которая вызывает exec(), остаются открытыми на протяжении выполнения этого вызова и доступны для использования в новой программе. Часто это может быть полезным, потому что файлы, открытые вызывающей программой в определенных дескрипторах, автоматически становятся доступными для новой программы (которая при этом не должна знать их имена или открывать их заново).
Командная оболочка пользуется этой возможностью для перенаправления ввода/вывода программ, которые в ней выполняются. Представьте, к примеру, что мы ввели следующую команду:
$ ls /tmp > dir.txt
Для ее выполнения командная оболочка проделывает следующие операции.
1. Выполняется вызов fork(), который создает дочерний процесс, представляющий собой копию командной оболочки (то есть в том числе и копию команды).
2. Дочерняя командная оболочка открывает для вывода файл dir.txt, используя файловый дескриптор 1 (стандартный вывод). Это можно сделать одним из двух способов.
• Дочерняя командная оболочка закрывает дескриптор 1 (STDOUT_FILENO), после чего открывает файл dir.txt. Поскольку вызов open() всегда использует наименьший доступный номер дескриптора, а стандартный ввод (дескриптор 0) остается открытым, файл будет открыт в дескрипторе 1.
Код будет выглядеть примерно так:
fd = open("dir.txt", O_WRONLY | O_CREAT,
S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
/* rw-rw-rw- */
if (fd!= STDOUT_FILENO) {
dup2(fd, STDOUT_FILENO);
close(fd);
}
• Командная оболочка открывает файл dir.txt, получая новый файловый дескриптор. Затем, если этот дескриптор не соответствует стандартному выводу, она использует вызов dup2(), чтобы принудить стандартный вывод стать дубликатом нового дескриптора. После этого новый дескриптор закрывается за ненадобностью (этот подход является более безопасным, чем предыдущий, поскольку он не полагается на открытие дескриптора с наименьшим номером).
3. Дочерняя командная оболочка выполняет команду ls, которая записывает результат своей работы в стандартный вывод, то есть в файл dir.txt.
Приводимое здесь объяснение того, как командная оболочка выполняет перенаправление ввода/вывода, упрощает некоторые моменты. В частности, некоторые так называемые встроенные команды выполняются оболочкой напрямую, без вызовов fork() или exec(). В контексте перенаправления ввода/вывода с ними нужно работать особым образом.