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##*/}
довольно неясна