Bash подстановка переменных vs dirname и basename

Следующий script

str=/aaa/bbb/ccc.txt
echo "str: $str"
echo ${str##*/} == $(basename $str)
echo ${str%/*} == $(dirname $str)

дает:

str: /aaa/bbb/ccc.txt
ccc.txt == ccc.txt
/aaa/bbb == /aaa/bbb

Возникает вопрос:

  • В сценариях bash, когда рекомендуется использовать команды dirname и basename, а также при замене переменных и почему?

Задача в основном потому, что:

str="/aaa/bbb/ccc.txt"
count=10000

s_cmdbase() {
let i=0
while(( i++ < $count ))
do
    a=$(basename $str)
done
}

s_varbase() {
let i=0
while(( i++ < $count ))
do
    a=${str##*/}
done
}

s_cmddir() {
let i=0
while(( i++ < $count ))
do
    a=$(dirname $str)
done
}

s_vardir() {
let i=0
while(( i++ < $count ))
do
    a=${str%/*}
done
}

time s_cmdbase
echo command basename
echo ===================================
time s_varbase
echo varsub basename
echo ===================================
time s_cmddir
echo command dirname
echo ===================================
time s_vardir
echo varsub dirname

в моей системе производит:

real    0m33.455s
user    0m10.194s
sys     0m18.106s
command basename
===================================

real    0m0.246s
user    0m0.237s
sys     0m0.007s
varsub basename
===================================

real    0m30.562s
user    0m10.115s
sys     0m17.764s
command dirname
===================================

real    0m0.237s
user    0m0.226s
sys     0m0.007s
varsub dirname

Вызов внешних программ (forking) требует времени. Основной вопрос заключается в следующем:

  • Есть ли какие-то подводные камни, использующие переменные подстановки вместо внешних команд?

Ответы

Ответ 1

Внешние команды делают некоторые логические исправления. Проверьте результат следующего script:

doit() {
    str=$1
    echo -e "string   $str"
    cmd=basename
    [[ "${str##*/}" == "$($cmd $str)" ]] && echo "$cmd same: ${str##*/}" || echo -e "$cmd different \${str##*/}\t>${str##*/}<\tvs command:\t>$($cmd $str)<"
    cmd=dirname
    [[ "${str%/*}"  == "$($cmd $str)" ]] && echo "$cmd  same: ${str%/*}" || echo -e "$cmd  different \${str%/*}\t>${str%/*}<\tvs command:\t>$($cmd $str)<"
    echo
}

doit /aaa/bbb/
doit /
doit /aaa
doit aaa
doit aaa/
doit aaa/xxx

с результатом

string   /aaa/bbb/
basename different ${str##*/}   ><          vs command: >bbb<
dirname  different ${str%/*}    >/aaa/bbb<  vs command: >/aaa<

string   /
basename different ${str##*/}   ><  vs command: >/<
dirname  different ${str%/*}    ><  vs command: >/<

string   /aaa
basename same: aaa
dirname  different ${str%/*}    ><  vs command: >/<

string   aaa
basename same: aaa
dirname  different ${str%/*}    >aaa<   vs command: >.<

string   aaa/
basename different ${str##*/}   ><  vs command: >aaa<
dirname  different ${str%/*}    >aaa<   vs command: >.<

string   aaa/xxx
basename same: xxx
dirname  same: aaa

Одним из наиболее интересных результатов является $(dirname "aaa"). Внешняя команда dirname корректно возвращает ., но расширение переменной ${str%/*} возвращает неверное значение aaa.

Альтернативное представление

Script:

doit() {
    strings=( "[[$1]]"
    "[[$(basename "$1")]]"
    "[[${1##*/}]]"
    "[[$(dirname "$1")]]"
    "[[${1%/*}]]" )
    printf "%-15s %-15s %-15s %-15s %-15s\n" "${strings[@]}"
}


printf "%-15s %-15s %-15s %-15s %-15s\n" \
    'file' 'basename $file' '${file##*/}' 'dirname $file' '${file%/*}'

doit /aaa/bbb/
doit /
doit /aaa
doit aaa
doit aaa/
doit aaa/xxx
doit aaa//

Вывод:

file            basename $file  ${file##*/}     dirname $file   ${file%/*}     
[[/aaa/bbb/]]   [[bbb]]         [[]]            [[/aaa]]        [[/aaa/bbb]]   
[[/]]           [[/]]           [[]]            [[/]]           [[]]           
[[/aaa]]        [[aaa]]         [[aaa]]         [[/]]           [[]]           
[[aaa]]         [[aaa]]         [[aaa]]         [[.]]           [[aaa]]        
[[aaa/]]        [[aaa]]         [[]]            [[.]]           [[aaa]]        
[[aaa/xxx]]     [[xxx]]         [[xxx]]         [[aaa]]         [[aaa]]        
[[aaa//]]       [[aaa]]         [[]]            [[.]]           [[aaa/]]       

Ответ 2

  • dirname выводит ., если его параметр не содержит косой черты /, поэтому эмуляция dirname с подстановкой параметров не дает одинаковых результатов в зависимости от ввода.

  • basename принимает суффикс как второй параметр, который также удалит этот компонент из имени файла. Вы можете эмулировать это также с помощью подстановки параметров, но поскольку вы не можете сделать оба одновременно, это не так кратко, как при использовании basename.

  • Использование dirname или basename требует подоболочки, поскольку они не являются оболочками, поэтому замена параметров будет быстрее, особенно при вызове их в цикле (как вы показали).

  • Я видел basename в разных местах в разных системах (/usr/bin, /bin), поэтому, если вам нужно использовать абсолютные пути в вашем script, по какой-то причине он может сломаться, так как он не может найти исполняемый файл.

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

EDIT: как dirname, так и basename фактически доступны как bash загружаемый builtin в examples/loadables в исходном дереве и могут быть включены (после компиляции) с использованием

enable -f /path/to/dirname dirname
enable -f /path/to/basename basename

Ответ 3

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

Это, конечно, субъективно! Лично я использую переменные замены по всему месту. Я использую read, IFS и set вместо awk. Я использую регулярные выражения bash, а bash расширенное globbing вместо sed. Но это потому, что:

a) Я хочу производительность

b) Я единственный человек, который когда-либо увидит эти сценарии

С грустью сказать, что многие люди, которые должны поддерживать shell-скрипты, мало знают об этом языке. Вы должны принять решение о балансе: что более важно, производительность или ремонтопригодность? В большинстве случаев вы обнаружите, что побеждает техническая поддержка.

Вы должны признать, что basename $0 довольно очевидна, тогда как ${0##*/} довольно неясна