Хотя стандартная библиотечная функция strtok
может быть полезна, вам нужно знать о недостатках ее интерфейса, что по сути является ловушкой для неосторожных.
В этой программе вы, похоже, наткнулись на обе наиболее распространенные проблемы с интерфейсом strtok
. Пожалуйста, перечитайте man strtok
внимательно в связи с этим ответом, чтобы не попасть в эти проблемы в будущем. Также не используйте strtok
в качестве примера хорошего дизайна интерфейса. Вместо этого используйте его как модель для того, чего следует избегать:
Скрытое глобальное состояние
strtok
работает с указателем строки, который хранится в статической переменной. Каждый раз, когда вы вызываете strtok
с ненулевым первым аргументом, он сначала сбрасывает значение этой статической переменной в эту строку. В конце каждого вызова strtok
он устанавливает в своей статической переменной адрес, с которого должно начинаться следующее сканирование, то есть сразу после только что найденного токена.
Во всей программе есть только один экземпляр статической переменной, поэтому вы не можете чередовать strtok
сканы в двух разных строках. Хуже того, вы не можете вызвать функцию, которая сама вызывает strtok
внутри strtok
сканирования строки, потому что вызов внутри функции сбросит состояние strtok
.
Это означает, что вы должны быть осторожны, когда у вас есть более одного strtok
сканирования в программе. В вашем случае после инициализации переменной с неправильным именем env
:
token = strtok(env, ":");
вы используете strtok
, чтобы разделить вашу команду ввода на части в переменной с плохим именем argv
:
argv = strtok(buf_copy, " ");
поэтому, когда вы позже захотите найти следующий компонент env
:
token = strtok(NULL, ":");
Состояние
strtok
больше не указывает на env
; вместо этого он указывает на buf_copy
(и, с вашим конкретным вводом, на точку в buf_copy
, где больше не будет найдено токенов).
Модификация входного аргумента
Первый аргумент strtok
- это char*
, а не const char*
.
Как правило, если библиотечная функция имеет строковый аргумент, аргумент должен быть объявлен как const char*
, если только функция не намеревается изменить строку. Или, другими словами, объявление const char*
- это обещание, что не будет предпринято никаких попыток изменить аргумент, и если это обещание не выполнено, это, вероятно, по уважительной причине.
И действительно, если вы прочитаете документацию strtok
, вы увидите, что он явно изменяет свою входную строку, перезаписывая некоторые символы-разделители символом NUL. Это приводит к постоянному разделению исходной строки на отдельные токены. Иногда это нормально, но это может привести к большим неприятностям, если вы захотите снова обратиться к исходному значению строки в будущем. Часто вы будете делать копию оригинальной строки, чтобы вызвать strtok
на ней. (Это часто является признаком плохого дизайна программы или сигналом того, что strtok
не совсем подходящий инструмент для разбора.)
В этой конкретной программе ловушка заключается в том, что getenv()
не возвращает копию значения переменной среды. Он возвращает указатель прямо в таблицу переменных среды. Хотя тип возвращаемого значения getenv
равен char*
, что может заставить вас поверить, что изменение значения в порядке, стандарт C явно запрещает:
Указанная строка не должна изменяться программой
К сожалению, этот запрет отсутствует на справочной странице Linux для getenv
, но на этой справочной странице указано, что getenv
дает вам указатель на таблицу окружения. Если вы действительно измените строку, возвращаемую getenv
, весьма вероятно (хотя и не гарантировано), что последующий вызов getenv
для той же переменной среды вернет измененное значение.
И это именно то, что вы делаете: так как вы позволили strtok
потерять строку, возвращаемую getenv(PATH)
, последующий вызов getenv(PATH)
увидит значение, усеченное в первом двоеточии.