Избегайте расширения при цикле: for x в $foo

Мне нужно подсчитать количество исполняемых файлов в каталоге.

Я уже нашел другой способ сделать это (записав в файл, а затем выполнив поиск в файле, но это довольно некрасиво).

Первое решение, которое пришло мне в голову, было таким (первый аргумент — путь к каталогу):

#!/bin/bash

noe=0

files=`ls -F $1` # because the -F option appends an indicator
                 # to the file, for an executable it's an '*'
                 # so if there is an executable 'script.sh' in the dir
                 # the output will be like this: 'script.sh*'

for i in $files
do
    if [ `echo "$i" | grep '*$'` ] #should search for an '*' at the end...
    then
        let noe += 1
    fi
done

echo $noe

Это не работает, потому что '*' опускается в цикле for.

(команда echo в цикле for выводит имя файла без '*' в конце, но нормально работает вне цикла for, когда аргумент находится в "")

Есть аналогичный вопрос об этом здесь , и мне удалось адаптировать ответ к моему случаю, но не объясняется, почему это нельзя сделать с for. + Я не совсем понимаю, зачем в цикле while дополнительный <

...
done < <(ls -F $1) 
     ^
     |_ I know that this means redirect to file to loop
        Does the second < mean that we are redirecting the
        standard input file? (this might be a stupid question)

Другой вопрос: есть ли способ обойти это с помощью цикла for и почему?


person Kok Nikol    schedule 22.05.2015    source источник
comment
Второй < является частью синтаксиса <(command) для подстановки процесса.   -  person Barmar    schedule 22.05.2015
comment
mywiki.wooledge.org/DontReadLinesWithFor   -  person Charles Duffy    schedule 22.05.2015


Ответы (3)


Решение этой проблемы ни при каких обстоятельствах не должно задействовать ls.

Вы можете перебирать файлы с помощью цикла for и использовать тест -x, чтобы определить, являются ли файлы исполняемыми. Однако каталоги обычно также являются исполняемыми (если это не так, вы не можете ввести их, например, с помощью cd), поэтому в зависимости от того, хотите ли вы включить каталоги в результат, вам также может понадобиться тест -d. Пример:

for file in ./*; do
    if [[ -x $file && ! -d $file ]]; then
        printf '<%s> is an executable file that is not a directory\n' "$file"
        (( count++ ))
    fi
done
printf '%d executable files found\n' "$count"

Что касается второго вопроса:

...
done < <(ls -F $1) 
     ^
     |_ I know that this means redirect to file to loop
        Does the second < mean that we are redirecting the
        standard input file? (this might be a stupid question)

<(...) — это подстановка процесса, и она заменяется именем файла на fd или именованный канал (в зависимости от того, что поддерживает операционная система). Любой процесс, который читает из этого fd или именованного канала, получит вывод (stdout) команды в пределах <(...)

Вы можете увидеть это, используя его с эхом:

$ echo <(true)  # turns into  echo /dev/fd/63
/dev/fd/63

Итак, в вашем случае done < <(ls...) превращается во что-то вроде done < /dev/fd/63, что является стандартным перенаправлением файлов, с которым вы уже знакомы.

person geirha    schedule 22.05.2015
comment
См. mywiki.wooledge.org/ParsingLs, чтобы узнать, почему использование ls таким образом — плохая идея. - person Etan Reisner; 22.05.2015
comment
@EtanReisner, да, эту страницу невозможно опубликовать достаточно. Добавил в первое предложение :) - person geirha; 22.05.2015

Используйте find для поиска файлов:

#!/bin/bash
find "$1" -type f -executable -maxdepth 1 | wc -l
person Walter A    schedule 22.05.2015

for x in $foo; do ...

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


Если бы вы отключили все расширения, вы бы получили это:

for x in "$foo"; do ...

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


Теперь здесь есть промежуточная позиция: вы можете временно отключить расширение глобуса, но оставить разделение строк на месте:

set -f              # disable all globbing
for x in $foo; do   # expand without globbing
  ...
done
set +f              # turn globbing back on

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


Если вы хотите построить массив, вы можете сделать это:

array=( "first element" "second element" "third element" )
for x in "${array[@]}"; do ...

...и в этом случае вы можете безопасно использовать for.

person Charles Duffy    schedule 22.05.2015