Нет-нет, этот пост совсем не про боль и страдание! Даже немножко наоборот. Просто картинка напоминает о значимости первой строчки будущего кода ).
Вначале, просто хотел описать свежую утилиту, с пылу с жару из под клавиатуры. Сама по себе она вполне ничего (хоть и до блеска бляхи новоиспечённого дембеля её ещё полировать и полировать), но описывать только её оказалось как-то скучновато. Потом решил, что на её примере можно поделиться башизмами, которыми сам раньше не пользовался. Далее подумал, что можно убить двух зайцев и описывать её совместно с жизненными примерами. Но в конце понял, что негоже мучить и без того изрядно потрёпанных зверушек, и решил просто их немножко причесать. Третий заяц (который и не заяц вовсе, а удав) мудро предпочёл воздержаться от участия в этом бардаке.
Так что если вдруг, тебе регулярно приходится искать файлы и может даже затем перемещать их куда-то; или тебе всё равно какой слой пыли лежит на файловой системе твоего сервера с аптаймом в несколько сотен лет и тебе просто интересен bash; или если ты просто мимо проходил{,а,о}, то не проходи мимо!
findNclean создан на базе великого и могучего find
, не менее великого mv
, и всё это в супе из bash
.
Конечно, можно было написать всё на python
и не заморачиваться с «тонкостями» bash
, но у меня не было такого выбора, плюс у bash получше с переносимостью между разными окружениями — просто исполняемый файл и никаких тебе библиотек, виртуальных окружений и сопутствующих проблем с запуском на винегрете из платформ. Кто не согласен — киньте в меня… чем-нибудь, только чтоб проняло :). Повозиться поэтому пришлось, включая адаптацию под старые окружения типа CentOS 5. Зато зависимостей получилось минимум, для новых осей (типа Ubuntu 16.04): find mv wc
, для старых плюс date touch
. Остальное всё чистый bash.
find
, который записывает найденные файлы в свой лог.APPROVED_SUFFIX
(во избежание перемещения файлов из не утверждённого списка), добавляешь этот суффикс к именам логов. По-умолчанию APPROVED_SUFFIX
пустой, т.е. переименование не требуется../findNclean
./findNclean help -c
# 100501й способ выстрелить себе в ногу
<Rule - На твой страх и риск>
result_list = /tmp/ненужное.csv
search_path = /
type = file
move_path = /.Trash
move_log = /var/log/findNclean - Highway to hell (remix).csv
</Rule>
2017-03-13T02:04:02 10853 Start action: find
2017-03-13T02:04:02 10853 Config: ./findNclean.conf.example
2017-03-13T02:04:02 10853 Debug mode enabled
2017-03-13T02:04:02 10853 Config rules count: 2
2017-03-13T02:04:02 10853 Relative paths allowed. Current dir: /home/user/path/to/findNclean
2017-03-13T02:04:02 10853 Effective config content:
<Global>
DEBUG = true
REL_PATH_ALLOW = true
APPROVED_SUFFIX = .approved
</Global>
<Rule1 find/move old unused *.tmp files in /tmp, /var and current user home dir>
result_list = ./ancient-tmp.find-example.csv
search_path = /var/
search_path = /tmp/
search_path = ~/
type = f
name = .*\.tmp
accessed < 2017
move_path = ./
move_log = ./ancient-tmp.move-example.csv
move_log_err = ./ancient-tmp.move-example.err.csv
</Rule1>
<Rule2 The same as above but for small non-zero logs>
result_list = ./ancient-logs.find-example.csv
search_path = ~/
search_path = /var/
search_path = /tmp/
type = f
name = .*\.log(\.gz)?
size > 0
size <= 2M
accessed < 2017
move_path = ./
move_log = ./ancient-logs.move-example.csv
move_log_err = ./ancient-logs.move-example.err.csv
</Rule2>
2017-03-13T02:04:02 10853 All non-zero size existing result_list files will be preliminary backuped:
2017-03-13T02:04:02 10853 The following commands will be evaluated (you can copy-paste them to bash terminal as is):
find -P -O3 -- /var/ /tmp/ /home/user/ -regextype posix-extended \( \! -name \*$'\n'\* -type f -regex .\*/.\*\\.tmp \! -newerat 2017-01-01T00:00:00 -fprintf ./ancient-tmp.find-example.csv %y\;%s\;%TY-%Tm-%TdT%TH:%TM:%TS\;%AY-%Am-%AdT%AH:%AM:%AS\;%u\;%g\;%p\\n \) , \( \! -name \*$'\n'\* -type f -regex .\*/.\*\\.log\(\\.gz\)\? -size +0c \( -size -2097152c -o -size 2097152c \) \! -newerat 2017-01-01T00:00:00 -fprintf ./ancient-logs.find-example.csv %y\;%s\;%TY-%Tm-%TdT%TH:%TM:%TS\;%AY-%Am-%AdT%AH:%AM:%AS\;%u\;%g\;%p\\n \)
2017-03-13T02:04:02 10853 Wait for all background processes completion...
2017-03-13T02:04:06 10853 Completed in 4 sec
2017-03-13T02:04:06 10853 Found items count (filenames with newline was excluded from the search):
0 ./ancient-tmp.find-example.csv
7 ./ancient-logs.find-example.csv
7 total
2017-03-13T02:04:06 10853 WARNING: Some 'find' commands returned non-zero exit code, results could be incomplete or incorrect!
2017-03-13T02:04:06 10853 Exit codes list: 1
2017-03-13T02:04:06 10853 Finished action: find
grep
, кстати, в этом контексте не совсем корректен, т.к. несколько дочерних процессов пишут в один и тот же терминал, и иногда один из процессов начинает/продолжает писать ещё до того как другой успел дописать строчку, поэтому часть полезной инфы может пропасть, а часть бесполезной стать видимой.vim ./ancient-logs.find-example.csv
mv ./ancient-logs.find-example.csv{,.approved}
mv ./ancient-tmp.find-example.csv{,.approved}
2017-03-13T02:05:32 11141 Start action: mv
2017-03-13T02:05:32 11141 Config: ./findNclean.conf.example
2017-03-13T02:05:32 11141 Debug mode enabled
2017-03-13T02:05:32 11141 Config rules count: 2
2017-03-13T02:05:32 11141 Relative paths allowed. Current dir: /home/user/path/to/findNclean
2017-03-13T02:05:32 11141 Effective config content:
<Global>
DEBUG = true
REL_PATH_ALLOW = true
APPROVED_SUFFIX = .approved
</Global>
<Rule1 find/move old unused *.tmp files in /tmp, /var and current user home dir>
result_list = ./ancient-tmp.find-example.csv
search_path = /var/
search_path = /tmp/
search_path = ~/
type = f
name = .*\.tmp
accessed < 2017
move_path = ./
move_log = ./ancient-tmp.move-example.csv
move_log_err = ./ancient-tmp.move-example.err.csv
</Rule1>
<Rule2 The same as above but for small non-zero logs>
result_list = ./ancient-logs.find-example.csv
search_path = ~/
search_path = /var/
search_path = /tmp/
type = f
name = .*\.log(\.gz)?
size > 0
size <= 2M
accessed < 2017
move_path = ./
move_log = ./ancient-logs.move-example.csv
move_log_err = ./ancient-logs.move-example.err.csv
</Rule2>
2017-03-13T02:05:32 11141 All non-zero size existing move_log and move_log_err files will be preliminary backuped:
2017-03-13T02:05:32 11141 Start processing the following move lists in background:
0 ./ancient-tmp.find-example.csv.approved
2 ./ancient-logs.find-example.csv.approved
2 total
2017-03-13T02:05:32 11141 Wait for all background processes completion...
mv: cannot move '/var/log/openvpn/00004-test.log' to './00004-test.log': Permission denied
mv: cannot move '/var/log/openvpn/00009-tmp.log' to './00009-tmp.log': Permission denied
2017-03-13T02:05:32 11141 Completed in 0 sec
2017-03-13T02:05:32 11141 Moved items count (successful canceled failed move_log [move_log_err]):
0 0 0 ./ancient-tmp.move-example.csv ./ancient-tmp.move-example.err.csv
0 0 2 ./ancient-logs.move-example.csv ./ancient-logs.move-example.err.csv
0 0 2 total
2017-03-13T02:05:32 11141 Finished action: mv
removed '/dev/shm/findNclean.exch26957.tmp'
removed '/dev/shm/findNclean.exch24396.tmp'
2017-03-13T02:06:48 11394 Start action: mv
2017-03-13T02:06:48 11394 Config: ./findNclean.conf.example
2017-03-13T02:06:48 11394 Debug mode enabled
2017-03-13T02:06:48 11394 Config rules count: 2
2017-03-13T02:06:48 11394 Relative paths allowed. Current dir: /home/user/path/to/findNclean
2017-03-13T02:06:48 11394 Effective config content:
<Global>
DEBUG = true
REL_PATH_ALLOW = true
APPROVED_SUFFIX = .approved
</Global>
<Rule1 find/move old unused *.tmp files in /tmp, /var and current user home dir>
result_list = ./ancient-tmp.find-example.csv
search_path = /var/
search_path = /tmp/
search_path = ~/
type = f
name = .*\.tmp
accessed < 2017
move_path = ./
move_log = ./ancient-tmp.move-example.csv
move_log_err = ./ancient-tmp.move-example.err.csv
</Rule1>
<Rule2 The same as above but for small non-zero logs>
result_list = ./ancient-logs.find-example.csv
search_path = ~/
search_path = /var/
search_path = /tmp/
type = f
name = .*\.log(\.gz)?
size > 0
size <= 2M
accessed < 2017
move_path = ./
move_log = ./ancient-logs.move-example.csv
move_log_err = ./ancient-logs.move-example.err.csv
</Rule2>
2017-03-13T02:06:48 11394 All non-zero size existing move_log and move_log_err files will be preliminary backuped:
2017-03-13T02:06:48 11394 Start processing the following move lists in background:
0 ./ancient-tmp.find-example.csv.approved
2 ./ancient-logs.find-example.csv.approved
2 total
2017-03-13T02:06:48 11394 Wait for all background processes completion...
2017-03-13T02:06:48 11394 Completed in 0 sec
2017-03-13T02:06:48 11394 Moved items count (successful canceled failed move_log [move_log_err]):
0 0 0 ./ancient-tmp.move-example.csv ./ancient-tmp.move-example.err.csv
2 0 0 ./ancient-logs.move-example.csv ./ancient-logs.move-example.err.csv
2 0 0 total
2017-03-13T02:06:48 11394 Finished action: mv
removed '/dev/shm/findNclean.exch24681.tmp'
removed '/dev/shm/findNclean.exch11002.tmp'
NOTIFICATION_SCRIPT
с путём ко скрипту, тогда ему будут передаваться параметры с которыми он может что-нибудь делать, например отправлять оповещения.IGNORE_FILES_WITH_NEWLINES = false
.read -r -d '' VARIABLE <<'EOF'
bla-bla line
EOF
VARIABLE=$(cat <<'EOF'
The bla-bla line
EOF
)
unset DESCRIPTION USAGE CONFIG_USAGE TRICKS # No need help variables yet
bash
это выглядит как-то так:printf -v BV '%d%03d%03d' "${BASH_VERSINFO[0]}" "${BASH_VERSINFO[1]}" "${BASH_VERSINFO[2]}" # Bash version in numbers like 4003046, where 4 is major version, 003 is minor, 046 is subminor.
((BV < 3002025)) && echo "WARNING: bash version ${BASH_VERSION} is too old (below 3.2.25)! This app was not tested with too ancient versions!" >&2
printf
в этом случае лучше, чем BV=$(bla-bla)
, ибо опять же встроенная утилита, и будет выполнена без запуска подоболочки. К тому же позволяет отформатировать исходные данные.int
с дальнейшим сравнением как с десятичным числом, если в у тебя в закромах такой завалялся — выкладывай!((BV < 3002025))
тоже лучше, чем [[ "${BV}" -lt 3002025 ]]
, т.к. быстрее, короче и человекочитаемей.printf %q
и соответственно eval
, но это своеобразная цена за поддержку почти всех символов, хотя не во всех местах эти конструкции так уж необходимы, часть из них были использованы по инерции.RULES_JOIN_ALLOW=1 # Allow optimize performance by join Rules with identical search_paths collection
if ((RULES_JOIN_ALLOW)); then
echo You'll see this msg
fi
$'\r'$'\n'
, их лучше использовать вместе со встроенной командой echo
вместо часто рекомендуемых escape-последовательностей для внешней утилиты /bin/echo
, вызываемой с помощью echo -e
(встроенная их не поддерживает). Но в закавыченных выражениях это несколько неудобно, придётся часто закрывать и открывать кавычки. Благо тот самый перенос строки можно назначить переменной и тогда никаких открываний-закрываний:readonly EOL=$'\n' # Newline
...
approve|confirm)
quit 255 "This action is not implemented yet.${EOL}You have to manually overview and approve list of files by appending${EOL}corresponding APPROVED_SUFFIX to the result_list value.${EOL}By default APPROVED_SUFFIX is none, so all result_list will be treated as approved."
read -r -d '' tmp <<-'EOF'
This action is not implemented yet.
You have to manually overview and approve list of files by appending
corresponding APPROVED_SUFFIX to the result_list value.
By default APPROVED_SUFFIX is none, so all result_list will be treated as approved.
EOF
quit 255 "${tmp}"
unset tmp
tmp='This action is not implemented yet.'$'\n'
tmp+='You have to manually overview and approve list of files by appending'$'\n'
tmp+='corresponding APPROVED_SUFFIX to the result_list value.'$'\n'
tmp+='By default APPROVED_SUFFIX is none, so all result_list will be treated as approved.'
quit 255 "${tmp}"
unset tmp
date
, хотя в новых версиях bash
для этого можно использовать printf %(datefmt)T
, правда без поддержки долей секунды:readonly TIMESTAMP_FORMAT='%Y-%m-%dT%H:%M:%S'
...
if ((BV > 4002000)); then # Modern bash versions
log() {
## Fast (builtin) but sec is min sample for most implementations
printf "%(${TIMESTAMP_FORMAT})T %5d %s\n" '-1' $$ "$*" # %b convert escapes, %s print as is
}
else # Legacy bash versions
log() {
## Slow (subshell, date) but support nanoseconds
echo "$(exec -c date +"${TIMESTAMP_FORMAT}") $$ $*"
}
fi
$(exec -c some command)
для запуска внешней утилиты в подоболочке. Это позволяет избавиться от дополнительного процесса-прослойки между текущим шеллом и утилитой, ну и просто быстрее (на 0-2% в зависимости от ситуации, но с миру по нитке на шапочку наберётся). Так будет выглядеть вырезка дерева процессов у команды $(some command)
:+-- /bin/bash # основной процесс
L-- /bin/bash # лишняя прослойка
L-- some command
exec
лишней прослойки не будет, а с опцией -c
команда будет запущена с пустым окружением, что тоже слегка ускорит работу.if ((BV > 4002000)); then # Modern bash versions
## Set global variable with the name $1 and time format $2
set_timestamp() {
printf -v "$1" "%($2)T" '-1'
}
else # Legacy bash versions
set_timestamp() {
printf -v "$1" '%s' "$(exec -c date +"$2")"
}
fi
...
set_timestamp ts '%s'
# код время исполнения которого измеряется
set_timestamp TS '%s'
log "Completed in $((TS-ts)) sec"
SECONDS
. Она равна 0 при старте оболочки и увеличивается на единицу каждую секунду. Если её обнулить, а потом обраться к ней, то получится время исполнения участка кода между этими событиями в секундах. Чтоб не терять текущее значение времени исполнения скрипта с самого начала можно также использовать промежуточные переменные:ts=${SECONDS}
# код время исполнения которого измеряется
TS=${SECONDS}
log "Completed in $((TS-ts)) sec"
set_timestamp
.while IFS= read -r line || [[ -n ${line} ]]; do
...
done </path/to/some/text/file
IFS
для read
нужна, чтобы не убирались начальные и конечные табы или пробелы;-r
— чтобы строка читалась как есть, без интерпретации escape-последовательностей типа \t\r
;|| [[ -n ${line} ]]
— чтобы код внутри цикла исполнялся даже если последняя строка файла не содержит переноса строки, без неё read
вернёт не нулевой код и цикл тут же завершится.born
): даже если файловая система (например ext4) поддерживает его и прилежно записывает при каждом создании нового файла, stat
и, соответственно, find
не могут его прочитать:user@host:~$stat /
Файл: '/'
Размер: 4096 Блоков: 8 Блок В/В: 4096 каталог
Устройство: fc00h/64512d Inode: 2 Ссылки: 25
Доступ: (0755/drwxr-xr-x) Uid: ( 0/ root) Gid: ( 0/ root)
Доступ: 2017-03-16 15:28:06.121675004 +0700
Модифицирован: 2017-02-27 17:09:09.937545795 +0700
Изменён: 2017-02-27 17:09:09.937545795 +0700
Создан: -
user@host:~$sudo debugfs -R "stat <$(stat -c %i /)>" /dev/ROOTFSDRIVE
Inode: 2 Type: directory Mode: 0755 Flags: 0x80000
Generation: 0 Version: 0x00000000:00000086
User: 0 Group: 0 Size: 4096
File ACL: 0 Directory ACL: 0
Links: 25 Blockcount: 8
Fragment: Address: 0 Number: 0 Size: 0
ctime: 0x58b3fac5:df87410c -- Mon Feb 27 17:09:09 2017
atime: 0x58ca4c96:1d0273f0 -- Thu Mar 16 15:28:06 2017
mtime: 0x58b3fac5:df87410c -- Mon Feb 27 17:09:09 2017
crtime: 0x5801149b:00000000 -- Sat Oct 15 00:23:39 2016
Size of extra inode fields: 32
EXTENTS:
(0):9249
mv
используется немного непривычным (для интерактивного терминала) образом:# Явное указание, что dest_name - файл
mv --backup=t -vT -- "${name}" "${dest_name}"
# Явное указание, что dest - директория, а всё, что после '--', - список источников к перемещению
mv --backup=t -vt "${dest}" -- "${name}"
# Когда ничего сохранять точно не надо,
# и даже заикаться об этом не стоит, смело перезаписывать в случае чего!
mv --backup=off -vft "${dest}" -- "${name}"
--backup[=CONTROL]
— полезная штука, в случае какого-то сбоя или непредвиденной ситуации вероятность вертать всё взад сильно возрастает.--
тоже полезно использовать в скриптах (это касается не только mv
, но и других), т.к. в какой-то степени отвязывает от использования ограниченного набора символов в значениях переменных (например если значение начинается с дефиса). В данном случае это не так уж необходимо, ибо у всех переменных для mv
вначале будет либо /
, либо ./
— во избежание, но лучше всё равно использовать такую возможность.which
, лучше использовать встроенный hash
:for util in ${DEPENDENCIES}; do
hash "${util}" &>/dev/null || quit 1 "ERROR: '${util}' not found on this system"
done; unset util
getopts
:OPTIND=2 # Ignore first argument ACTION even if it has leading hyphen
while getopts ':dvc:' OPT; do
[[ "${OPTARG:0:1}" = '-' ]] && quit 1 "ERROR: Option argument cannot start with hyphen, got: ${OPTARG}"
case "${OPT}" in
d|v) DEBUG=1; v=v; ;;
c) CONFIG=${OPTARG} ;;
:) quit 1 "ERROR: Option -${OPTARG} requires an argument"; ;;
*) quit 1 "ERROR: Unrecognized option: -${OPTARG}"
esac
done
--
).${var#word} # Убрать самый короткий префикс
${var##word} # Убрать самый длинный префикс
${var%word} # Убрать самый короткий суффикс
${var%%word} # Убрать самый длинный суффикс
${var/pattern/string} # Поиск и замена
${var^pattern} # К верхнему регистру первый символ совпадения
${var^^pattern} # К верхнему регистру все символы совпадения
${var,pattern} # К нижнему регистру первый символ совпадения
${var,,pattern} # К нижнему регистру все символы совпадения
${var~} # Инвертировать регистр для первого символа
${var~~} # Инвертировать регистр для всех символов
parallel_run "${Commands[@]/#/exec -c }" # Prepending of 'exec -c ' need to avoid additional subshell creation
exec -c
'.$((${MOVE_OK_COUNT[@]/#/+}))
bash
ещё полно подводных камней и нырять за ними можно пока не утонешь даже если ты осилил man bash
и засим случайные прохожие регулярно снимают перед тобой шляпу.BUGS
It's too big and too slow.
#!/bin/bash
К сожалению, не доступен сервер mySQL