bash: безпечне процедурне використання знаходження у select


12

Враховуючи ці назви файлів:

$ ls -1
file
file name
otherfile

bash сама по собі прекрасно справляється із вбудованим пробілом:

$ for file in *; do echo "$file"; done
file
file name
otherfile
$ select file in *; do echo "$file"; done
1) file
2) file name
3) otherfile
#?

Однак іноді, можливо, я не хочу працювати з кожним файлом, або навіть суворо в ньому $PWD, куди findвходить. Що також обробляє пробіл номінально:

$ find -type f -name file\*
./file
./file name
./directory/file
./directory/file name

Я намагаюся придумати безпечну для цього версію версію цього сценарію, яка візьме висновок findта представить його в select:

$ select file in $(find -type f -name file); do echo $file; break; done
1) ./file
2) ./directory/file

Однак це вибухає з пробілами у файлах:

$ select file in $(find -type f -name file\*); do echo $file; break; done
1) ./file        3) name          5) ./directory/file
2) ./file        4) ./directory/file  6) name

Зазвичай я б обійшов це, возившись IFS. Однак:

$ IFS=$'\n' select file in $(find -type f -name file\*); do echo $file; break; done
-bash: syntax error near unexpected token `do'
$ IFS='\n' select file in $(find -type f -name file\*); do echo $file; break; done
-bash: syntax error near unexpected token `do'

Яке рішення для цього?



1
Якщо ви тільки з допомогою findсвоєї здатності відповідати певне ім'я файлу, ви можете просто використовувати select file in **/file*(після установки shopt -s globstar) в bash4 або пізнішої версії.
чепнер

Відповіді:


14

Якщо вам потрібно лише обробляти пробіли та вкладки (не вбудовані нові рядки), тоді ви можете використовувати mapfile(або його синонім, readarray) для читання в масив, наприклад, заданий

$ ls -1
file
other file
somefile

потім

$ IFS= mapfile -t files < <(find . -type f)
$ select f in "${files[@]}"; do ls "$f"; break; done
1) ./file
2) ./somefile
3) ./other file
#? 3
./other file

Якщо робити потрібно обробляти переклади рядків, і ваша bashверсія забезпечує нуль-роздільники mapfile1 , то ви можете змінити що IFS= mapfile -t -d '' files < <(find . -type f -print0). В іншому випадку збирайте еквівалентний масив із обмеженого на нуль findвиводу за допомогою readциклу:

$ touch $'filename\nwith\nnewlines'
$ 
$ files=()
$ while IFS= read -r -d '' f; do files+=("$f"); done < <(find . -type f -print0)
$ 
$ select f in "${files[@]}"; do ls "$f"; break; done
1) ./file
2) ./somefile
3) ./other file
4) ./filename
with
newlines
#? 4
./filename?with?newlines

1-d опція була додана до mapfileв bashверсії 4.4 IIRC


2
+1 для іншого дієслова, яке я раніше не використовував
roaima

Дійсно, mapfileце і для мене новий. Кудос.
DopeGhoti

while IFS= readВерсія працює ще в Баш v3 (що дуже важливо для тих , хто з нас з допомогою MacOS).
Гордон Девіссон

3
+1 для find -print0варіанту; бурчить за розміщення його після відомої неправильної версії та описує її лише для використання, якщо хтось знає, що їм потрібно обробляти нові рядки. Якщо хтось обробляє лише несподіване в тих місцях, де очікується, ніколи взагалі ніколи не поводиться з несподіваним.
Чарльз Даффі

8

У цій відповіді є рішення для будь-якого типу файлів. З новими рядками чи пробілами.
Існують рішення як для недавнього, так і для давнього башмаку та навіть старих оболонок пози.

Дерево, перелічене нижче в цій відповіді [1] , використовується для тестів.

виберіть

Легко приступити selectдо роботи або з масивом:

$ dir='deep/inside/a/dir'
$ arr=( "$dir"/* )
$ select var in "${arr[@]}"; do echo "$var"; break; done

Або з позиційними параметрами:

$ set -- "$dir"/*
$ select var; do echo "$var"; break; done

Отже, єдина реальна проблема - отримати "список файлів" (правильно розмежований) всередині масиву або всередині позиційних параметрів. Продовжуйте читати.

баш

Я не бачу проблеми, про яку ви повідомляєте bash. Bash може шукати всередині заданого каталогу:

$ dir='deep/inside/a/dir'
$ printf '<%s>\n' "$dir"/*
<deep/inside/a/dir/directory>
<deep/inside/a/dir/file>
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/zz last file>

Або, якщо вам подобається цикл:

$ set -- "$dir"/*
$ for f; do printf '<%s>\n' "$f"; done
<deep/inside/a/dir/directory>
<deep/inside/a/dir/file>
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/zz last file>

Зауважте, що вищевказаний синтаксис буде правильно працювати з будь-якою (розумною) оболонкою (не принаймні csh).

Єдине обмеження, яке має синтаксис вище, - це спуститися до інших каталогів.
Але Баш міг це зробити:

$ shopt -s globstar
$ set -- "$dir"/**/*
$ for f; do printf '<%s>\n' "$f"; done
<deep/inside/a/dir/directory>
<deep/inside/a/dir/directory/file>
<deep/inside/a/dir/directory/file name>
<deep/inside/a/dir/directory/file with a
newline>
<deep/inside/a/dir/directory/zz last file>
<deep/inside/a/dir/file>
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/zz last file>

Щоб вибрати лише деякі файли (наприклад, ті, що закінчуються у файлі), просто замініть *:

$ set -- "$dir"/**/*file
$ printf '<%s>\n' "$@"
<deep/inside/a/dir/directory/file>
<deep/inside/a/dir/directory/zz last file>
<deep/inside/a/dir/file>
<deep/inside/a/dir/zz last file>

міцний

Коли ви розміщуєте " безпечний для космосу " в заголовку, я припускаю, що те, що ви мали на увазі, " надійне ".

Найпростіший спосіб бути надійним щодо пробілів (або нових рядків) - відхилити обробку вводу, який містить пробіли (або нові рядки). Дуже простий спосіб зробити це в оболонці - це вихід з помилкою, якщо будь-яке ім'я файлу розширюється пробілом. Існує кілька способів зробити це, але найбільш компактний (і posix) (але обмежений одним вмістом каталогів, включаючи назви судових каталогів та уникаючи точкових файлів):

$ set -- "$dir"/file*                            # read the directory
$ a="$(printf '%s' "$@" x)"                      # make it a long string
$ [ "$a" = "${a%% *}" ] || echo "exit on space"  # if $a has an space.
$ nl='
'                    # define a new line in the usual posix way.  

$ [ "$a" = "${a%%"$nl"*}" ] || echo "exit on newline"  # if $a has a newline.

Якщо застосовуваний розчин є надійним у будь-якому з цих предметів, видаліть тест.

В bash, підкаталоги можуть бути протестовані одразу з ** поясненим вище.

Є кілька способів включити крапкові файли, рішення Posix таке:

set -- "$dir"/* "$dir"/.[!.]* "$dir"/..?*

знайти

Якщо знахідку потрібно використовувати з якоїсь причини, замініть роздільник на NUL (0x00).

баш 4.4+

$ readarray -t -d '' arr < <(find "$dir" -type f -name file\* -print0)
$ printf '<%s>\n' "${arr[@]}"
<deep/inside/a/dir/file name>
<deep/inside/a/dir/file with a
newline>
<deep/inside/a/dir/directory/file name>
<deep/inside/a/dir/directory/file with a
newline>
<deep/inside/a/dir/directory/file>
<deep/inside/a/dir/file>

баш 2,05+

i=1  # lets start on 1 so it works also in zsh.
while IFS='' read -d '' val; do 
    arr[i++]="$val";
done < <(find "$dir" -type f -name \*file -print0)
printf '<%s>\n' "${arr[@]}"

ПОСІБНО

Для того, щоб зробити дійсне рішення POSIX, коли знахідка не має роздільника NUL і немає -d(ні -a) для читання, нам потрібен цілком різний підхід.

Нам потрібно використовувати комплекс -execвід пошуку з викликом до оболонки:

find "$dir" -type f -exec sh -c '
    for f do
        echo "<$f>"
    done
    ' sh {} +

Або, якщо потрібно - це вибір (select є частиною bash, не sh):

$ find "$dir" -type f -exec bash -c '
      select f; do echo "<$f>"; break; done ' bash {} +

1) deep/inside/a/dir/file name
2) deep/inside/a/dir/zz last file
3) deep/inside/a/dir/file with a
newline
4) deep/inside/a/dir/directory/file name
5) deep/inside/a/dir/directory/zz last file
6) deep/inside/a/dir/directory/file with a
newline
7) deep/inside/a/dir/directory/file
8) deep/inside/a/dir/file
#? 3
<deep/inside/a/dir/file with a
newline>

[1] Це дерево (\ 012 - це нові рядки):

$ tree
.
└── deep
    └── inside
        └── a
            └── dir
                ├── directory
                   ├── file
                   ├── file name
                   └── file with a \012newline
                ├── file
                ├── file name
                ├── otherfile
                ├── with a\012newline
                └── zz last file

Можна побудувати за допомогою цих двох команд:

$ mkdir -p deep/inside/a/dir/directory/
$ touch deep/inside/a/dir/{,directory/}{file{,\ {name,with\ a$'\n'newline}},zz\ last\ file}

6

Ви не можете встановити змінну перед циклічною конструкцією, але ви можете встановити її перед умовою. Ось сегмент зі сторінки man:

Навколишнє середовище для будь-якої простої команди чи функції може бути тимчасово доповнене, додавши її до призначення параметрів, як описано вище в PARAMETERS.

(Цикл не проста команда .)

Ось поширена конструкція, що демонструє сценарії відмов і успіху:

IFS=$'\n' while read -r x; do ...; done </tmp/file     # Failure
while IFS=$'\n' read -r x; do ...; done </tmp/file     # Success

На жаль, я не бачу способу вбудувати зміни IFSв selectконструкцію, впливаючи на обробку пов'язаного $(...). Однак, нічого не заважає IFSвстановлюватись поза циклом:

IFS=$'\n'; while read -r x; do ...; done </tmp/file    # Also success

і саме ця конструкція, з якою я бачу, працює з select:

IFS=$'\n'; select file in $(find -type f -name 'file*'); do echo "$file"; break; done

При написанні оборонного коду , який я рекомендував би , що положення або працювати в субоболочке, або IFSі SHELLOPTSзбережено і відновлено навколо блоку:

OIFS="$IFS" IFS=$'\n'                     # Split on newline only
OSHELLOPTS="$SHELLOPTS"; set -o noglob    # Wildcards must not expand twice

select file in $(find -type f -name 'file*'); do echo $file; break; done

IFS="$OIFS"
[[ "$OSHELLOPTS" !~ noglob ]] && set +o noglob

5
Якщо припустити, що IFS=$'\n'це безпечно, це безпідставно. Імена файлів відмінно вміють містити літерали нового рядка.
Чарльз Даффі

4
Я відверто вагаюся прийняти такі твердження про можливий набір даних за номіналом, навіть коли він присутній. Найгірша подія втрати даних, в якій я був присутній, - це випадок, коли сценарій технічного обслуговування, відповідальний за очищення старих резервних копій, намагався видалити файл, створений сценарієм Python, використовуючи модуль C з поганим перенапрямленням покажчика, який скидав випадковий сміття - включаючи підкреслений пробіл, відокремлений пробілом - у назву.
Чарльз Даффі

2
Люди, що будують сценарій оболонки, роблячи очищення цих файлів, не намагалися цитувати, тому що імена "не могли" не збігатися [0-9a-f]{24}. ТБ резервного копіювання даних, що використовуються для підтримки рахунків клієнтів, втрачено.
Чарльз Даффі

4
Погодьтеся з @CharlesDuffy повністю. Не обробляти крайові корпуси добре лише тоді, коли ви працюєте в інтерактивному режимі і бачите, що ви робите. selectза своєю конструкцією призначений для сценаріїв рішення, тому він завжди повинен бути розроблений для обробки крайових корпусів.
Wildcard

2
@ilkkachu, звичайно - ти ніколи не дзвониш selectіз оболонки, де ти вводиш команди для запуску, а лише у сценарії, де ти відповідаєш на запит, наданий цим сценарієм , і де цей сценарій виконання заздалегідь визначеної логіки (побудованої без знання назви файлів, якими керуються) на основі цього входу.
Чарльз Даффі

4

Я можу бути поза моєю юрисдикцією тут, але, можливо, ви можете почати з чогось подібного, принаймні, це не має жодних проблем з пробілом:

find -maxdepth 1 -type f -printf '%f\000' | {
    while read -d $'\000'; do
            echo "$REPLY"
            echo
    done
}

Щоб уникнути можливих помилкових припущень, як зазначено в коментарях, пам’ятайте, що вищевказаний код еквівалентний:

   find -maxdepth 1 -type f -printf '%f\0' | {
        while read -d ''; do
                echo "$REPLY"
                echo
        done
    }

read -d- розумне рішення; спасибі за це.
DopeGhoti

2
read -d $'\000'це точно ідентично read -d '', але вводить в оману людей про можливості в Bash (маючи на увазі, неправильно, що це може представляти буквальні NULs всередині рядків). Запустіть s1=$'foo\000bar'; s2='foo', а потім спробуйте знайти спосіб розрізнити два значення. (Майбутня версія може нормалізуватися з поведінкою підстановки команд, зробивши збережене значення еквівалентним foobar, але це не так сьогодні).
Чарльз Даффі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.