IFS и подстановка команд

Я пишу сценарий оболочки для чтения входных CSV-файлов и запуска java-программы соответственно.

#!/usr/bin/ksh
CSV_FILE=${1}
myScript="/usr/bin/java -version"
while read row
do
     $myScript
     IFS=$"|"
     for column in $row
     do 
        $myScript
     done
done < $CSV_FILE

CSV-файл:

a|b|c

Интересно, что $myScript вне цикла for работает, но $myScript внутри цикла for говорит: «/usr/bin/java -версия: не найдено [Нет такого файла или каталога]». Я узнал, что это потому, что я устанавливаю IFS. Если я прокомментирую IFS и изменю файл csv на

a b c

Оно работает ! Я предполагаю, что оболочка использует IFS по умолчанию для разделения команды /usr/bin/java, а затем применяет аргумент -version позже. Поскольку я изменил IFS, он принимает всю строку как одну команду — по крайней мере, я думаю, что это происходит.

Но это мое требование: у меня есть файл csv с настраиваемым разделителем, а в команде есть аргументы, разделенные пробелом. Как я могу сделать это правильно?


person Mayavi    schedule 08.09.2018    source источник
comment
Почему вы сохраняете команду в переменной в первую очередь? См. Часто задаваемые вопросы по Bash #50.   -  person PesaThe    schedule 08.09.2018
comment
@PesaThe Большое спасибо, я новичок в написании сценариев оболочки, и эта ссылка была полезной.   -  person Mayavi    schedule 08.09.2018


Ответы (4)


IFS указывает, как разделить значения переменных в заменах без кавычек. Это относится как к $row, так и к $myscript.

Если вы хотите использовать IFS для разделения, что удобно в простом sh, вам нужно изменить значение IFS или сделать так, чтобы оно требовало того же значения. В этом конкретном случае вы можете легко добиться того же значения, определив myScript как myScript="/usr/bin/java|-version". Кроме того, вы можете вовремя изменить значение IFS. Обратите внимание, что в обоих случаях подстановка без кавычек не просто разделяет значение с помощью IFS, но также интерпретирует каждую часть как шаблон подстановки и заменяет его списком совпадающих имен файлов, если таковые имеются. Это означает, что если ваш файл CSV содержит строку вида

foo|*|bar

тогда строка будет не foo, *, bar, а foo, каждое имя файла в текущем каталоге bar. Чтобы обрабатывать данные таким образом, вам нужно отключить с помощью set -f. Также помните, что read считывает строки продолжения, когда строка заканчивается обратной косой чертой, и удаляет начальные и конечные IFS символы. Используйте IFS= read -r, чтобы отключить эти два поведения.

myScript="/usr/bin/java -version"
set -f
while IFS= read -r row
do
    $myScript
    IFS='|'
    for column in $row
    do 
        IFS=' '
        $myScript
    done
done

Однако есть лучшие способы полностью избежать разделения IFS. Не храните команду в строке, разделенной пробелами: она не работает в сложных случаях, например, в командах, которым требуется аргумент, содержащий пробел. Существует три надежных способа хранения команды:

  • Сохраните команду в функции. Это самый естественный подход. Выполнение команды — это код; вы определяете код в функции. Вы можете ссылаться на аргументы функции вместе как "$@".

    myScript () {
        /usr/bin/java -version "$@"
    }
    …
    myScript extra_argument_1 extra_argument_2
    
  • Сохраните имя исполняемой команды и ее аргументы в массиве.

    myScript=(/usr/bin/java -version)
    …
    "${myScript[@]}" extra_argument_1 extra_argument_2
    
  • Сохраните команду оболочки, то есть то, что предназначено для анализа оболочкой. Чтобы оценить шелл-код в строке, используйте eval. Обязательно заключайте аргумент в кавычки, как и любое другое раскрытие переменной, чтобы избежать преждевременного раскрытия подстановочных знаков. Этот подход более сложен, так как требует тщательного цитирования. Это действительно полезно, только когда вам нужно сохранить команду в строке, например, потому что она входит в качестве параметра вашего скрипта. Обратите внимание, что вы не можете разумно передавать дополнительные аргументы таким образом.

    myScript='/usr/bin/java -version'
    …
    eval "$myScript"
    

Кроме того, поскольку вы используете ksh, а не обычный sh, вам не нужно использовать IFS для разделения строки ввода. Вместо этого используйте read -A для прямого разделения на массив.

#!/usr/bin/ksh
CSV_FILE=${1}
myScript=(/usr/bin/java -version)
while IFS='|' read -r -A columns
do
    "${myScript[@]}"
    for column in "${columns[@]}"
    do 
        "${myScript[@]}"
    done
done <"$CSV_FILE"
person Gilles 'SO- stop being evil'    schedule 08.09.2018

IFS сообщает оболочке, какие символы разделяют «слова», то есть разные компоненты команды. Поэтому, когда вы удаляете символ пробела из IFS и запускаете foo bar, сценарий видит один аргумент "foo bar", а не "foo" и "bar".

person l0b0    schedule 08.09.2018

Самое простое решение — не изменять IFS и выполнять разбиение с помощью read -d <delimiter> следующим образом:

#!/usr/bin/ksh
CSV_FILE=${1}
myScript="/usr/bin/java -version"
while read -A -d '|' columns
do
     $myScript
     for column in "${columns[@]}"
     do 
        echo next is "$column"
        $myScript
     done
done < $CSV_FILE
person A.H.    schedule 08.09.2018

IFS следует размещать за "пока"

#!/usr/bin/ksh
CSV_FILE=${1}
myScript="/usr/bin/java -version"
while IFS="|" read row
do
 $myScript
 for column in $row
 do 
    $myScript
 done
done < $CSV_FILE
person Ivan    schedule 08.09.2018
comment
Хотя это несколько на правильном пути, это не работает. Поскольку read row считывает только одну переменную, установка IFS только для нее не влияет на разделение. Строка по-прежнему разделена в начале цикла for. Смотрите мой ответ, чтобы узнать, как это может работать. - person Gilles 'SO- stop being evil'; 08.09.2018