Пользуешься командным интерпретатором каждый день? Готов решить несколько логических задачек и узнать что-то новое? Добро пожаловать под кат.
Часть представленных здесь задач не принесёт реальной пользы, так как затрагивает какие-то сложные граничные случаи. Другая же часть будет полезна тем, кто постоянно использует шелл и читает чужие скрипты.
Примечание: на момент написания статьи автор использовал bash 4.4.12(1)-release в подсистеме Linux на Windows 10. Сложность задач различная.
Потоки ввода-вывода
Задача 1
$ cat 1
The cake is a lie!
Wanted!
Cake or alive
$ cat 1 | head | tail | sed -e 's/alive/dead/g' | tee | wc -l > 1
Сколько строк будет в файле 1 после выполнения команды?
1
очищается перед запуском первой программы и cat
открывает уже очищенный файл. Задача 2
$ cat file1
I love UNIX!
$ cat file2
I don't like UNIX
$ cat file1 <file2
Что будет выведено на экран?
I love UNIX!
Задача 3
$ cat file
Just for fun
$ cat file 1>&2 2>/dev/null
Что будет выведено на экран?
Just for fun
1>&2
перенаправляет первый поток во второй, однако, это не так. Рассмотрим команду из задания. В начале интерпретации введённой команды таблица потоков выглядит так:0 | 1 | 2 |
stdin | stdout | stderr |
bash обнаруживает последовательность
1>&2
и копирует содержимое ячейки 2 в ячейку 1:0 | 1 | 2 |
stdin | stderr | stderr |
После обнаружения последовательности
2>/dev/null
интерпретатор записывает значение в ячейку 2, оставляя другие ячейки нетронутыми:0 | 1 | 2 |
stdin | stderr | /dev/null |
bash выводит так же и поток ошибок, так что на мы обнаруживаем на экране текст файла.
Задача 4
Как вывод stdout отправить на stderr, а вывод stderr, наоборот, на stdout?
4>&1 1>&2 2>&4
Исполняемые файлы
Задача 5
Дан файл test.sh
#!/bin/bash
ls $*
ls $@
ls "$*"
ls "$@"
Выполняются команды:
$ ls
1 2 3 test.sh
$ ./test.sh 1 2 3
Что выведет скрипт?
1 2 3
1 2 3
ls: cannot access '1 2 3': No such file or directory
1 2 3
Задача 6
Создадим в текущей директории файл
-c
c правами 755 и таким содержимым:#!/bin/bash
echo $1
Обнулим переменную $PATH и попытаемся выполнить:
$ PATH=
$ -c "echo SURPRISE"
Что будет выведено на экран? Что произойдет, если повторить ввод последней команды?
SURPRISE
, второй раз echo SURPRISE
<shebang> <filename> <args>
Таким образом, перед выполнением наша команда выглядит так:
/bin/bash -c "echo SURPRISE"
И, как следствие, выполняется совершенно не то, что мы хотели.
Если выполнить второй раз, то шелл подберёт информацию о -c из кэша и выполнит его уже верно. Единственный способ защититься от столь неожиданного эффекта — добавить два минуса в шебанг.
Переменные
Задача 7
$ ls
file
$ cat <$(ls)
$ cat <(ls)
Что будет выведено на экран в первом и во втором случае?
cat <file
Во втором случае
<(ls)
будет заменён на именованный пайп, соединённый входом с stdout ls, и выходом с stdin cat.После подстановки команда приобретёт вид:
cat /dev/fd/xx
Задача 8
$ TEST=123456
$ echo ${TEST%56}
Что будет выведено на экран?
$ TEST=file.ext
$ echo ${TEST%.ext}
file
Задача 9
$ echo ${friendship:-magic}
Что будет выведено на экран?
Порядок выполнения
Задача 10
while true; false; do
echo Success
done
Что будет выведено на экран?
Задача 11
$ false && true || true && false && echo 1 || echo 2
Что будет выведено на экран?
((((false && true) || true) && false) && echo 1) || echo 2
(((false || true) && false) && echo 1) || echo 2
((true && false) && echo 1) || echo 2
(false && echo 1) || echo 2
false || echo 2
echo 2
Замечания, пожелания и дополнительные задачи приветствуются в комментарии или ЛС.
Комментарии (30)
cagami
13.06.2018 12:33Firemoon
а разве при перенаправлении потока баш хоть что-то возвращает?
мне казалось, что вообще никогда ничего не выплевывает в консоль
добавим в ваш код не перезапись файла, а добавление.
cat 1 | head | tail | sed -e 's/alive/dead/g' | tee | wc -l >> 1
:~# cat 1
The cake is a lie!
1Firemoon Автор
13.06.2018 12:53а разве при перенаправлении потока баш хоть что-то возвращает?
Эээ. Смотря какого потока и куда.
добавим в ваш код не перезапись файла, а добавление.
И всё заработает, потому что баш откроет файл на дозапись, и содержимое не пострадает.
cagami
13.06.2018 20:55ну сейчас у вас почему-то другой вопрос
Сколько строк будет в файле 1 после выполнения команды?
но и тут же опять не правильно.
в вашем пример строк будет 1
и в ней будет записан будет 0(число)
SlavniyTeo
13.06.2018 15:22а разве при перенаправлении потока баш хоть что-то возвращает?
мне казалось, что вообще никогда ничего не выплевывает в консольВсе верно, первая задача некорректно составлена.
$ cat 1 The cake is a lie! $ cat 1 | head | tail | sed -e 's/alive/dead/g' | tee | wc -l > 1
Ничего на экран не выведет, так же как и (здесь важно
> 2
в конце):
$ cat 1 The cake is a lie! $ cat 1 | head | tail | sed -e 's/alive/dead/g' | tee | wc -l > 2
Не зависимо от открытия/очищения файлов.
Просто потому что &1 направлен в файл вместо stdout.
erwin_shrodinger
13.06.2018 12:33По поводу 3 задачи.
Есть заблуждение, что последовательность 1>&2 перенаправляет первый поток во второй, однако, это не так.
bash обнаруживает последовательность 1>&2 и копирует содержимое ячейки 2 в ячейку 1
Ой ли?
Здесь вот совсем другое говорят:
i>&j
# Redirects file descriptor i to j.
# All output of file pointed to by i gets sent to file pointed to by j.
В вашем примере stdout перенаправляется и в 1 и в 2 дескрипторы. Затем 2 перенаправляется в /dev/null. Но 1 как содержал в себе stdout, так и содержит.saboteur_kiev
13.06.2018 14:59Именно. Правда корректнее считать, что 1 содержал в себе не STDOUT, а уже конкретный терминал типа /dev/pts/1
Firemoon Автор
13.06.2018 12:51Хм. Давайте разберёмся.
Сначала цитата про заблуждение. Я встречал людей, которые ещё недостаточно постигли работу с шеллом и поэтому думают, что перенаправление происходит именно потоков, то есть `1>&2` в их понимании значит «слить во второй поток, второй поток сам разберётся», именно на них нацелена данная задача.
Далее, то, что говорят [вон там](https://www.tldp.org/LDP/abs/html/io-redirection.html).
> gets sent to file pointed to by j.
То есть перенаправление происходит в файл, на который указывает в данный момент j-тый дескриптор. Если j-тый дескриптор станет указывать на другой файл, i-тый останется без изменений.
>В вашем примере stdout перенаправляется и в 1 и в 2 дескрипторы. Затем 2 перенаправляется в /dev/null. Но 1 как содержал в себе stdout, так и содержит.
А вот тут, честно, не понял, откуда stdout? В объяснении есть табличка с дескрипторами.
UPD: erwin_shrodinger, пардон, я промахнулся веткой.erwin_shrodinger
13.06.2018 13:00А вот тут, честно, не понял, откуда stdout? В объяснении есть табличка с дескрипторами.
Согласен, написал некорректно. Имелось в виду вот что.
По вашему утверждению (судя по табличке) ситуация будет такая:
0 ~ stdin
1 ~ stderr
2 ~ stderr
Я же утверждаю обратное:
0 ~ stdin
1 ~ stdout
2 ~ stdout
В таком случае, после 2>/dev/null мы не теряем stdout, так как можем получить его из 1-го дескриптора.
Т.е. в целом мне ваш «финт» с 1>&2 кажется нефункциональным и уж тем более не копирующим содержимое ячейки 2 в ячейку 1.
UPD: Вот тут вы сами же это подтверждаете:
То есть перенаправление происходит в файл, на который указывает в данный момент j-тый дескриптор. Если j-тый дескриптор станет указывать на другой файл, i-тый останется без изменений
saboteur_kiev
13.06.2018 15:01STDOUT и STDERR это названия потоков для программиста, который пишет программу.
В то время как у запущенного процесса уже нет STDIN, STDOUT, STDERR — есть открытые файловые дескрипторы 0, 1, 2 которые на при создании терминала сразу ассоциируются с конкретным устройством.
Поэтому переключая 2 в 1 вы переключаете не STDERR в STDOUT, а 2 дескриптор в то, куда сейчас смотрит 1-й дескриптор.
Посмотреть куда смотрят дескрипторы можно в директории fd процесса (file descriptors) — вот так
ls -l /proc/$$/fd
saboteur_kiev
13.06.2018 14:26+1Первая же задача
$ cat 1 | head | tail | sed -e 's/alive/dead/g' | tee | wc -l > 1
После интерпретации команды, но до запуска всех программ bash работает с указанными потоками ввода-вывода. Таким образом файл 1 очищается перед запуском первой программы и cat открывает уже очищенный файл.
Не совсем согласен с объяснением. Да, файл будет очищен, но данная команда в конечном счете весь вывод на экран перенаправляет в > 1, следовательно на экран не будет ничего выведено, даже если в конце перенаправить в другой файл (например > 2.txt), не затирая 1.
saboteur_kiev
13.06.2018 14:36Задача 3
$ cat file 1>&2 2>/dev/null
just for fun
Объяснение
Есть заблуждение, что последовательность 1>&2 перенаправляет первый поток во второй, однако, это не так.
Не совсем согласен с объяснением. IMHO правильнее говорить, что вывод из программы у нас идет не в STDOUT и STDERR, а вот с точки зрения запущенной в консоли программы, она пользуется дескрипторами 1 и 2, которые можно посмотреть вот так
ls -l /proc/$$/fd
То можно увидеть, что эти дескрипторы — просто ссылки на /dev/pts/X.
Поскольку мы сперва перенаправляем 1>&2, когда у нас &2 все еще /dev/pts/X, он будет перенаправлен в /dev/pts/X (ничего не изменится), и только затем мы меняем 2>/dev/null.
Если же поменять перенаправления местами, то 1> тоже перенаправится в /devnull, поскольку к этому моменту дескриптор 2 будет уже ссылаться на него.
$ cat file 2>/dev/null 1>&2
Такая команда ничего не выведет.
saboteur_kiev
13.06.2018 14:38Задача 4
Как вывод stdout отправить на stderr, а вывод stderr, наоборот, на stdout?
Ответ
4>&1 1>&2 2>&4
Объяснение
Принцип ровно как и в предыдущей задаче. Именно поэтому нам требуется дополнительный поток для временного хранения.
Опять не очень понятная задача — у нас нет STDERR и STDOUT с точки зрения консоли, есть открытые файловые дескрипторы 1 и 2, которые уже куда-то ссылкаются. Чтобы поменять их местами, можно просто посмотреть куда конкретно они ссылкаются и назначить. То есть да, указанная команда сработает, но объяснение — не совсем понятное почему оно так сработает.
saboteur_kiev
13.06.2018 15:03P.S. А вообще — статья полезная!
Сейчас в комментариях разберемся как следуети накажем кого попало!
Sirikid
14.06.2018 03:01А можно было подписать "11 причин не использовать bash"
win32nipuh
14.06.2018 08:49Что использовать вместо bash?
Sirikid
16.06.2018 04:15POSIX sh или любой адекватный скриптовой язык для переносимых скриптов, fish для терминала и личных скриптов.
kvaps
14.06.2018 13:05Сразу вспомнилась эта картинка :)
Скрытый текстUranix
14.06.2018 10:29Пожалуйста, объясните, что именно кэшируется в задаче 6, и как это на результат работы влияет?
saboteur_kiev
14.06.2018 13:14Bash кеширует полный путь ко всем выполняемым командам. Вы можете посмотреть текущий кеш командой hash (команда встроенная, справка по help hash)
Ключевой момент — это то, что при пустом PATH, баш начинает искать исполняемые файлы в текущем каталоге, поэтому он найдет файл -c и выполнит его, затем выполнить вторую команду (в bash можно выполнить две команды, разделенные пробелом)
То есть первый вызов команды
-с «echo Hello»
будет на самом деле вызов двух независимых команд:
"./-c" и «echo Hello»
Первая не выведет ничего, так как ей не будет передан аргумент, а вторая выведет Hello
Но в кеш у нас сохранится -c, которая находится в ./-c. И в следующий раз оно будет выполнять -c уже как вызов скрипта, а не как команду. А при вызове скрипта, ему будут передаваться аргументы
Во второй раз команда
-c «echo Hello»
будет выполняться уже как:
./-c «echo Hello»
Следовательно будет выполнен echo «echo Hello» из скрипта
В этом примере используется как минимум три малоизвестных нюанса
1. Например выполнение команд через пробел. Посмотрите пример ниже:
скрипт show_var.sh (не забудьте chmod 755 show_var.sh)
#!/bin/bash
echo $MYVAR
в консоли
$ MYVAR=123 ./show_var
123
$ echo $MYVAR
2. То, что при пустом PATH поиск исполняемых файлов происходит в текущем каталоге — я сам не знал.
3. То, что выполненные внешние команды кешируются в bash (путь к ним)Uzix
15.06.2018 12:10Не соглашусь с некоторыми тезисами в объяснении. Ключевое в задаче №6 то, как происходит запуск скрипта и передача аргументов ему же. Первым аргументом команде, прописанной в shebang (#!/bin/bash) передаётся собственно имя скрипта. При первом запуске передаётся "-c", что интерпретируется как аргумент командной строки, выполняющий следующую за ним команду внутри bash. Т.е. полная команда будет выглядеть так: '/bin/bash -c «echo SURPRISE»'. При последующих запусках bash берёт уже путь из кеша, и команда будет выглядеть так: '/bin/bash ./-c «echo SURPRISE»'. Здесь "./-c" уже интерпретируется как имя скрипта для выполнения, а всё что дальше — как аргументы скрипта.
Выполнение команд через пробел работает, насколько мне известно, только для установки переменных окружения.
Вообще, передача "-c" как имени файла при пустом кеше выглядит как баг.saboteur_kiev
15.06.2018 16:59При первом запуске никому ничего не передается.
Выполняется команда "-c", а не шебанг и имя скрипта.
Еще раз.
1. Залогинились,
2. У нас в каталоге есть исполняемый файл "-c"
3. Обнулили PATH
Выполняем
-c «echo Hello»
Как эту строку разбирает интерпретатор?
Он просто начинает выполнять команды по очереди. Команды через пробел — тоже команды, и он выполняет команду "-c"
На этом этапе он понятия не имеет, что это за команда — внутренней такой команды нет, внешней в кеше нет, поэтому он просто пытается ее выполнить. Так как при пустом PATH у нас происходит поиск команд в текущем каталоге, запускается файл ./-c, который не выводит ничего — ему ничего не передавали.
Затем выполняется команда «echo Hello»
Теперь запускаем это во второй раз.
Интерпретатор видит "-c", но у него в кеше есть путь ./-c — поэтому он знает, что это внешний исполняемый файл. А если это внешний исполняемый файл, то значит ему всю оставшуюся строку нужно передать как аргументы. В прошлый раз он этого не знал, поэтому и не передавал.
Собственно, если вы сомневаетесь — никто ж не мешает вам проверить на практике, И написать скрипт, который выводит не echo $1, а echo «trulyalya», и увидеть, что
-c «echo Hello»
при первом запуске выведет Hello, а не trulyalyaUzix
15.06.2018 17:23Приведу 3 примера, ставящих под сомнение утверждение о выполнении команд через пробел интерпретатором.
1. Попробуйте переименовать "-c" в, например, "-r". Первый и последующие запуски будут давать одинаковый результат.
2. Попробуйте следующий скрипт:
Первый и последующие запуски также будут давать одинаковый результат.#!/bin/bash -- echo $1
3. Попробуйте следующий скрипт:
При первом запуске, несмотря на присутствие uname в скрипте, будет напечатано лишь «SURPRISE», при последующих — «Linux».#!/bin/bash /bin/uname
Programmer74
15.06.2018 01:18Firemoon Автор, как насчет результатов работы этих команд в других нетрадиционных шеллах типа ksh?)
kvazimoda24
Не понял, откуда в восьмой задаче в выводе взялись цифры 8 и 9.
Firemoon Автор
Когда изменял задания, забыл перенести ответ. Исправил, благодарю