Почему bash игнорирует символы новой строки при выполнении цикла for над содержимым строки в стиле C? - PullRequest
5 голосов
/ 30 октября 2009

Почему следующее ...

c=0; for i in $'1\n2\n3\n4'; do echo iteration $c :$i:; c=$[c+1]; done

распечатать ...

iteration 0 :1 2 3 4:

а не

iteration 0 :1:
iteration 1 :2:
iteration 2 :3:
iteration 3 :4:

Из того, что я понимаю, синтаксис $ 'STRING' должен позволять мне указывать строку с escape-символами. Не следует ли интерпретировать "\ n" как символ новой строки, чтобы цикл for выводился четыре раза, по одному разу для каждой строки? Вместо этого кажется, что символ новой строки интерпретируется как пробел.

Я принял предложение и попытался установить $ IFS. Результаты были одинаковыми.

IFS=$'\n'; c=0; for i in $'1\n2\n3\n4'; do echo iteration $c :$i:; c=$[c+1]; done; unset IFS;

iteration 0 :1 2 3 4:

В своем комментарии Уильям Пурссел говорит, что это не сработало, потому что IFS был переведен на новую строку ... но последующее не сработало.

IFS=' '; c=0; for i in '1 2 3 4'; do echo iteration $c :$i:; c=$[c+1]; done; unset IFS;

iteration 0 :1 2 3 4:

Использование IFS = '' в строке, разделенной символом новой строки, привело к еще большему беспорядку ...

IFS=' '; c=0; for i in $'1\n2\n3\n4'; do echo iteration $c :$i:; c=$[c+1]; done; unset IFS;

iteration 0 :1
2
3
4:

установка IFS на '\ n' вместо $ '\ n' имела тот же эффект, что и IFS = '' ...

IFS='\n'; c=0; for i in $'1\n2\n3\n4'; do echo iteration $c :$i:; c=$[c+1]; done; unset IFS;

iteration 0 :1
2
3
4:

Есть только одна итерация, но по какой-то причине в эхо-строке видна новая строка.

То, что сработало, сначала сохранило строку в переменной, затем зациклило содержимое переменной (без необходимости устанавливать IFS):

c=0; v=$'1\n2\n3\n4'; for i in $v; do echo iteration $c :$i:; c=$[c+1]; done

iteration 0 :1:
iteration 1 :2:
iteration 2 :3:
iteration 3 :4:

Что еще не объясняет, почему существует эта проблема.

Здесь есть шаблон? Является ли это ожидаемым поведением IFS, как определено в ссылке «Размотка»?

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

Полагаю, это объясняет, почему строковые литералы не разделяются для итерации цикла, независимо от того, какие экранирующие символы используются. Только когда литерал присваивается переменной, эта переменная раскрывается для деления в цикле for. Я думаю, также с подстановкой команд.

Примеры:

Результат подстановки команды делится

c=0; for i in `echo $'1\n2\n3\n4'`; do echo iteration $c :$i:; c=$[c+1]; done

iteration 0 :1:
iteration 1 :2:
iteration 2 :3:
iteration 3 :4:

Часть раскрытой строки разделена, остальные - нет.

c=0; v=$'1 \n\t2\t3 4'; for i in $v$'\n5\n6'; do echo iteration $c :$i:; c=$[c+1]; done

iteration 0 :1:
iteration 1 :2:
iteration 2 :3:
iteration 3 :4 5 6:

Когда раскрытие происходит в двойных кавычках, расщепление не происходит.

c=0; v=$'1\n2\n3 4'; for i in "$v"; do echo iteration $c :$i:; c=$[c+1]; done

iteration 0 :1 2 3 4:

Любая последовательность SPACE, TAB, NEWLINE используется в качестве разделителя для разделения.

c=0; v=$'1 2\t3 \t\n4'; for i in $v; do echo iteration $c :$i:; c=$[c+1]; done

iteration 0 :1:
iteration 1 :2:
iteration 2 :3:
iteration 3 :4:

Я приму ответ отмотки, так как его ссылка дает ответ на мой вопрос.

Не знаю, почему поведение эхо-сигнала в цикле for изменяется со значением IFS.

РЕДАКТИРОВАТЬ: расширен для уточнения.

Ответы [ 5 ]

7 голосов
/ 30 октября 2009

В этом контексте Bash не выполняет раскрытие слов для строк в кавычках. Например:

$ for i in "a b c d"; do echo $i; done
a b c d

$ for i in a b c d; do echo $i; done
a
b
c
d

$ var="a b c d"; for i in "$var"; do echo $i; done
a b c d

$ var="a b c d"; for i in $var; do echo $i; done
a
b
c
d

В комментарии вы заявили, что "IFS = '\ n' также работает. Что не работает, так это IFS = $ '\ n'. Я сейчас очень запутался."

В IFS='\n' вы устанавливаете разделители (множественное число) для двух символов обратной косой черты и «n». Поэтому, если вы сделаете это (вставив «X» в середине «\ n»), вы увидите, что произойдет. Он обрабатывает последовательности "\ n" буквально, несмотря на то, что они у вас есть в $'':

$ IFS='\n'; for i in $'a\Xnb\nc\n'; do echo $i; done; rrifs
a X b
c

Редактировать 2 (в ответ на комментарий):

Он видит '\n' как два символа (не перевод строки) и $'a\Xnb\nc\n' как буквальную строку из 10 символов (без перевода строки), тогда echo выводит строку и интерпретирует последовательность "\ n" как перевод строки (поскольку строка «помечена» для интерпретации), но поскольку она заключена в кавычки, она рассматривается как одна строка, а не как слова, разделенные $IFS.

Попробуйте их для дальнейшего сравнения:

$ c=0; for i in "a\nb\nc\n"; do echo -e "iteration $c :$i:"; c=$[c+1]; done
iteration 0 :a
b
c
:

$ c=0; for i in "a\nb\nc\n"; do echo "iteration $c :$i:"; c=$[c+1]; done
iteration 0 :a\nb\nc\n:

$ c=0; for i in a\\nb\\nc\\n; do echo -e "iteration $c :$i:"; c=$[c+1]; done
iteration 0 :a
b
c
:

$ c=0; for i in a\\nb\\nc\\n; do echo "iteration $c :$i:"; c=$[c+1]; done
iteration 0 :a\nb\nc\n:

Настройка IFS не влияет на вышеуказанное.

Это работает (обратите внимание, что $var не заключено в кавычки в операторе for):

$ var=$'a\nb\nc\n'
$ saveIFS="$IFS"   # it's important to save and restore $IFS
$ IFS=$'\n'        # set $IFS to a newline using $'\n' (not '\n')
$ c=0; for i in $var; do echo -e "iteration $c :$i:"; c=$[c+1]; done
iteration 0 :a:
iteration 1 :b:
iteration 2 :c:
$ IFS="$saveIFS"
6 голосов
/ 30 октября 2009

Измените настройку <a href="http://www.faqs.org/docs/bashman/bashref_33.html" rel="nofollow noreferrer">$IFS</a>, чтобы изменить способ разделения текста на слова в bash.

Примечание редактора:
Этот ответ был принят, потому что он предоставляет ссылку на информацию, которая в конечном итоге объясняет основные проблемы.
Однако обратите внимание, что проблему OP можно , а не , решить, просто изменив $IFS, поскольку $IFS не относится к цитируемым строкам .

0 голосов
/ 13 мая 2017

полезный ответ Денниса Уильямсона полностью объясняет симптомы, и даже сам вопрос сейчас в основном дает; ответ mouviciel хорошо сводит проблемы, но (на момент написания статьи) содержит неверную информацию о $IFS.
Поэтому позвольте мне попробовать сводку правил , которые применяются , с последующим подробным анализом:

  • С цитируемыми строками , независимо от стиля цитирования, IFS, внутренний разделитель полей никогда не вступает в игру .

    • Строка в кавычках в качестве единственного драйвера for цикла всегда приводит к одиночной итерации с назначением (потенциально расширенной) строки в целом к переменной цикла.
  • Разделение строк на слова по символам-разделителям, указанным в $IFS ( разбиение слов ) относится только к результатам расширений без кавычек , а именно:

    • ссылки на переменные без кавычек ($var), называемые расширениями параметров (включая преобразования, такие как удаление префиксов и суффиксов, подстановки, ...)
    • подстановки команд без кавычек ($(...) или в старом стиле `...`)
    • арифметические расширения без кавычек ($(( ... )) - обратите внимание, что синтаксис $[...] устарел и его следует избегать).
  • Чтобы назначать управляющие символы, такие как <newline> и <tab>, $IFS, , используйте строку ANSI C-кавычки ($'...') , который понимает escape-последовательности , такие как \n и \t; например, IFS=$'\n'; напротив, IFS='\n' назначит 2 литеральных символов: литерал \ и литерал n (строки в одинарных кавычках всегда используют свое содержимое буквально).

Обратите внимание, что если бы команда echo в исходном коде использовала один аргумент в двойных кавычках (echo "iteration $c :$i:"), то $IFS не был бы применен вообще, что позволило бы избежать путаницы.


Анализ команды из вопроса:

c=0; for i in $'1\n2\n3\n4'; do echo iteration $c :$i:; c=$[c+1]; done
  • $IFS и разделение слов только применяются к команде echo, а не к петле for.

  • Строка ANSI C в кавычках $'1\n2\n3\n4', так как драйвер цикла приводит к следующей строке из 4 строк, назначенной $i:

    1
    2
    3
    4
    
  • echo iteration $c :$i: из-за наличия только аргументов без кавычек заставляет оболочку подвергать их разбиению по словам , а также globbing ( расширение имени файла ; хотя это не имеет никакого эффекта в данном конкретном случае):

    • $c, поскольку содержит только 0 (в одной и той же итерации), не изменяется в процессе.

    • :$i:, напротив, на основе $IFS, содержащего <space><tab><newline> по умолчанию, разбивается на 4 отдельных слова : :1, 2, 3, и 4: - обратите внимание, как включающий : стал частью первого и последнего слова.

    • Примечание. Для используйте значение переменной как есть, всегда ставьте двойную кавычку ссылку на переменную.
      Расщепление и подстановка слов являются примерами расширений оболочки , что является общим термином для предварительной интерпретации аргументов оболочкой.

  • echo поэтому вручается 6 отдельных аргументов : iteration, 0 и :1, 2, 3 и 4:. В output , echo объединяет свои аргументы с единственным пробелом (не связанным с $IFS), получая iteration 0 :1 2 3 4:


Как правильно написать цикл

Обратите внимание на двойные кавычки строки, переданной в echo, и встроенное арифметическое расширение, которое объединяет сообщение о текущем значении $c с последующим его увеличением ($((c++))).

Если значения итерации известны заранее:

# Simply use an unquoted, space-separated list (the indiv. elements may be quoted, however).
c=0; for i in 1 2 3 4; do echo "iteration: $((c++)) :$i:"; done

# Alternative, with an array:
vals=( 1 2 3 4 )
c=0; for i in "${vals[@]}"; do echo "iteration: $((c++)) :$i:"; done

# If the iteration values form a range of numbers, you can also use
# brace expansion (`for i in {1..4}...`) or, better for larger ranges
# and required for variable-based endpoints, a C-style loop (`for ((i=0;i<4;++i))...`)

Если значения итерации НЕ известны заранее:

Использование for для циклического перебора строк ввода не рекомендуется , поскольку использование раскрытия без кавычек потребует от вас устранения возможных нежелательных разбиений и разбивки слов, а также потому, что весь ввод Перед запуском цикла необходимо прочитать все данные в память.

Цикл while, для которого строки предоставляются через stdin, является лучшим выбором (<<< - это here-string , строка, которая передается через stdin):

c=0; while IFS= read -r i; do echo "iteration: $((c++)) :$i:"; done <<<$'1\n2\n3\n4'

read читает строку за строкой, а -r в сочетании с IFS= (отключение разделения слов путем установки его в нулевую строку) гарантирует, что каждая строка будет прочитана полностью, как есть.
Обратите внимание, что при добавлении IFS= непосредственно к read его значение локализуется в этой команде , без изменения значения $IFS текущей оболочки - это универсальный механизм в POSIX-совместимых оболочках .

0 голосов
/ 31 октября 2009

попробуй

c=0; for i in $'1\\n2\\n3\\n4'; do echo -e iteration $c :$i:; c=$[c+1]; done

дополнительные обратные слэши сохраняют выходы для новых строк, echo -e говорит эхо расширять переходы.

0 голосов
/ 30 октября 2009

Две причины:

  1. Ваш цикл for зацикливается только один раз: цикл содержит только один элемент - строку $'1\n2\n3\n4'. Если вы хотите выполнить цикл четыре раза, вам нужно изменить $IFS, как предложено для размотки.

  2. echo принимает эту строку и интерпретирует ее как четыре аргумента, разделенных символами новой строки. Затем отображаются все аргументы, разделенные пробелами. Если вы хотите, чтобы echo не интерпретировал входную строку, поместите ее в двойные кавычки, как в echo "$i".

Редактировать, после вопроса редактировать:

  • Я пытался изменить $IFS: это работало, но я использовал export $IFS='\n'

  • Во втором случае $v интерпретируется командой bash в команде for, которая интерпретирует его как четыре аргумента, разделенных символами новой строки. Если вы хотите получить свою первую проблему снова, просто используйте for f in "$v" вместо for f in $v.

...