bashman

Пикабушник
281 рейтинг 16 подписчиков 8 подписок 3 поста 0 в горячем
Награды:
5 лет на Пикабу
23

Разбор скрипта для командной строки Linux. Часть 3(заключительная)

Вот и пришло время заключительной части. В 1-ой части мы сделали код более читабельным, во 2-ой части мы избавились от множества регулярных выражений и лишнего кода. В этой части, мы избавимся от временных файлов и распределим вывод.

Во второй части я совсем забыл про не столь критичные, но всё же замечания в комментариях. Так вместо #!/bin/bash советовали #!/usr/bin/env bash и вместо [ ] использовать [[ ]]. Перед началом разбора, применим эти замечания и получим:

#!/usr/bin/env bash

####PART 1####

if [[ -z "$1" ]]; then
echo "Error: missing argument" 1>&2 ; exit 1
fi
f_out="$(mktemp)"
f_tmp="$(mktemp)"
trap "rm -f $f_out $f_tmp" EXIT

####PART 2####

echo "*********************************************"
echo "Get A records from DNS:"
dig +short {www.,}$1 A | tee "$f_out"

####PART 3####

echo "*********************************************"
echo "Get NS records from DNS:"
dig +short $1 NS | while read nsserv
do
nsname=${nsserv:0:-1}
echo "=================================="
echo "NS: $nsname"
dig +short @$nsname $1 A | tee -a $f_out
done

####PART 4####

echo "*********************************************"
echo "Resolve ip range from whois service:"
sort -n -t . -k 1,1 -k 2,2 -k 3,3 -k 4,4 $f_out | uniq > $f_tmp
rm $f_out
cat $f_tmp | while read ip
do
echo "Get ip range for $ip"
whois $ip | grep -E -i "inetnum|route|netrange|cidr" >> $f_out
done

####PART 5####

echo "*********************************************"
echo "Result"
echo "*********************************************"
sort $f_out | uniq | while read range
do
echo "${range:16}"
done

Для начала, хотелось бы сказать, что для больших данных, запись в файл может быть предпочтительнее, чем хранение в переменной. Но в нашем случае информации не так много, поэтому лучше использовать переменные и(или) массивы. Также стоит заменить, что все команды echo, лучше заменить на команду printf.

В 1-ой части кода происходит создание временных файлов, что для нас уже не актуально и можно удалить. Получим:

if [[ -z "$1" ]]; then

printf -- "%s\n" "Error: missing argument" 1>&2 ; exit 1

fi

Далее в каждой из частей происходит выписывание звездочек, заголовка и результата операции. В таком варианте ясно какие шаги выполняет скрипт, но скрипт будет сложнее использовать, т.к. нельзя будет просто записать конечный результат в файл. Поэтому, лучше выписывать промежуточные этапы не на stdout, а на stderr. Так мы улучшим скрипт и не повлияем на функциональность, т.к. всегда можно сделать  2>&1. Cделаем для этого функцию verbose:

function verbose {
printf -- "%s\n" "$1" 1>&2
}

Звездочки с заголовком выписываются в каждой части, поэтому для этого добавим функцию print_header:

function print_header {

verbose "*********************************************"

verbose "$1"

}

Теперь изменим 2-ую часть. Вместо временных файлов запишем результат в перемененную records. Так мы получим:

####PART 2####

print_header "Get A records from DNS:"


records="$(dig +short {www.,}$1 A)"

verbose "$records"

Поработаем над 3-ей частью, пошлём вывод на stderr и добавим новые  ip в переменную records:

####PART 3####
print_header "Get NS records from DNS:"


dig +short $1 NS | while read nsserv
do
nsname=${nsserv:0:-1}
verbose "=================================="
verbose "NS: $nsname"
ns_records="$(dig +short @$nsname $1 A)"
verbose "$ns_records"
records+=ns_records
done

А вот с 4-ой частью возникли проблемы. В данной строке:

whois $ip | grep -E -i "inetnum|route|netrange|cidr" >> $f_out

после проведение grep, терялся \n на конце последней строки. В этом примере это не было проблемой, т.к. >> добавлял текст в конец файла и сам ставил новую строку(\n) между старым и новым текстом. Но когда я переписал на переменные, то проблема дала о себе знать и пришлось добавлять символ \n для каждого результата этой команды. Однако появилась одна пустая строка в конце результата, но удалить её было просто:

result="${result%$'\n'}

4-ая часть после изменений:

####PART 4####

print_header "Resolve ip range from whois service:"

records="$(sort -n -t . -k 1,1 -k 2,2 -k 3,3 -k 4,4 <<<"$records" | uniq)"

result=''

while read ip

do

verbose "Get ip range for $ip"

result+="$(whois $ip | grep -E -i "inetnum|route|netrange|cidr")"$'\n'

done <<< "$records"


result="${result%$'\n'}

Немного изменим 5-ую часть:

####PART 5####
print_header "Result:"
sort <<< "$result" | uniq | while read range
do
printf -- "%s\n" "${range:16}"
done

После всех изменений, мы получим:

#!/usr/bin/env bash

function verbose {

printf -- "%s\n" "$1" 1>&2
}

function print_header {

verbose "*********************************************"
verbose "$1"
}

####PART 1####

if [[ -z "$1" ]]; then
printf -- "%s\n" "Error: missing argument" 1>&2 ; exit 1
fi

####PART 2####

print_header "Get A records from DNS:"
records="$(dig +short {www.,}$1 A)"
verbose "$records"

####PART 3####

print_header "Get NS records from DNS:"
dig +short $1 NS | while read nsserv
do
nsname=${nsserv:0:-1}
verbose "=================================="
verbose "NS: $nsname"
ns_records="$(dig +short @$nsname $1 A)"
verbose "$ns_records"
records+=ns_records
done

####PART 4####

print_header "Resolve ip range from whois service:"
records="$(sort -n -t . -k 1,1 -k 2,2 -k 3,3 -k 4,4 <<<"$records" | uniq)"
result=''
while read ip
do
verbose "Get ip range for $ip"
result+="$(whois $ip | grep -E -i "inetnum|route|netrange|cidr")"$'\n'
done <<< "$records"
result="${result%$'\n'}"

####PART 5####

print_header "Result:"
sort <<< "$result" | uniq | while read range
do
printf -- "%s\n" "${range:16}"
done

Возможно код стал немного хуже читаться, но за-то мы избавились от временных файлов. А стоит ли оно того, решать только вам.

Заключение:

Последняя часть возможно вышла слегка сумбурна, но сказывается то, что на весь разбор ушло значительно больше времени, чем планировалось. Если с глазу на глаз можно быстро объяснить суть ошибок, то данный формат заставляет думать над каждым словом и писать предельно чётко. Однако, вижу что данный разбор, для достаточного для меня количества людей, пришёлся по душе и это радует.


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

Спасибо всем за внимание и поддержку, надеюсь данный разбор оказался для вас полезен!

Показать полностью
18

Разбор скрипта для командной строки Linux. Часть 2

В прошлой части разбора нам удалось сократить и сделать код более читабельным. В этой части, мы попробуем изучить используемые в скрипте инструменты и сделать код ещё лучше.


Хоть скрипт теоретически был рассчитан на любые доменные имена. На деле, проверка на доменной имя pikabu.ru, дала следующий результат:

91.228.152.0 - 91.228.155.255
MNT-FIRSTCOLO
91.228.152.0/22

Как видим, 2-я строчка тут явно лишняя. Из этого делаем вывод, что скрипт не работает правильно для всех доменов. В вопросе нахождения сетей, которые принадлежат домену, я увы недостаточно компетентен. Поэтому моей целью будет лишь добиться схожего результата и разобрать общие проблемы, которые актуальны для большинства скриптов.

И так, код который мы получили в конце первой части:

#!/bin/bash

####PART 1####

if [[ -z "$1" ]]; then
echo "Error: missing argument" 1>&2 ; exit 1
fi

f_out="$(mktemp)"

f_tmp="$(mktemp)"

trap "rm -f $f_out $f_tmp" EXIT

####PART 2####

echo "*********************************************"
echo "Get A records from DNS:"
# Octet regex
o_re="(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"
# IP regex
ip_re="$o_re\.$o_re\.$o_re\.$o_re"

dig $1 A | grep "^$1" | grep -o -E "$ip_re" | tee "$f_out"

dig $1 A | grep "^www.$1" | grep -o -E "$ip_re" | tee -a "$f_out"

####PART 3####

echo "*********************************************"
echo "Get NS records from DNS:"
dig $1 NS | grep "^$1" | awk {'print $NF}' | while read nsserv
do
nsname=${nsserv:0:${#nsserv}-1}
echo "=================================="
echo "NS: $nsname"
dig @$nsname $1 A | grep "^$1" | grep -o -E "$ip_re" | tee -a $f_out
done
####PART 4####
echo "*********************************************"
echo "Resolve ip range from whois service:"
sort -h $f_out | uniq > $f_tmp
rm $f_out
cat $f_tmp | while read ip
do
echo "Get ip range for $ip"
whois $ip | grep -E -i "inetnum|route|netrange|cidr" >> $f_out
done


####PART 5####

echo "*********************************************"

echo "Result"
echo "*********************************************"
sort $f_out | uniq | while read range
do
echo "${range:16}"
done

Плюс ко всему, код был разделён на 5 частей, для лучшей навигации.

Для начала, вернемся ко 2-ой части кода. А конкретно к этим строкам:

dig $1 A | grep "^$1" | grep -o -E "$ip_re" | tee "$f_out"
dig $1 A | grep "^www.$1" | grep -o -E "$ip_re" | tee -a "$f_out"

Лишь на данном моменте тут была замечена ошибка. Предположительно, автор хотел получить данные как по доменному имени vk.com, так и www.vk.com, однако забыл дописать это программе dig. Внесем правки:

dig $1 A | grep "^$1" | grep -o -E "$ip_re" | tee "$f_out"
dig www.$1 A | grep "^www.$1" | grep -o -E "$ip_re" | tee -a "$f_out"

Далее было замечено, что программа dig умеет принимать множество доменных имен, воспользуемся этим:

dig {www.,}$1 A | grep -E "^(www.|)$1" | grep -o -E "$ip_re" | tee "$f_out"

Уже лучше, вместо 2-ух строк, теперь лишь одна.


Программа dig выписывает множество лишней информации:

; <<>> DiG 9.9.5-9+deb8u6-Debian <<>> vk.com A
;; global options: +cmd
;; Got answer:
;; ->>HEADER<<- opcode: QUERY, status: NOERROR, id: 10732
;; flags: qr rd ra; QUERY: 1, ANSWER: 3, AUTHORITY: 0, ADDITIONAL: 1
;; OPT PSEUDOSECTION:
; EDNS: version: 0, flags:; udp: 1280
;; QUESTION SECTION:
;vk.com. IN A
;; ANSWER SECTION:
vk.com. 33 IN A 87.240.131.118
vk.com. 33 IN A 87.240.131.117
vk.com. 33 IN A 87.240.131.120
;; Query time: 0 msec
;; SERVER: 192.168.3.1#53(192.168.3.1)
;; WHEN: Fri Jun 24 16:27:13 CEST 2016
;; MSG SIZE rcvd: 83

Автор дальнейшими действиями извлекает из этого вывода ip адреса. Но если посмотреть мануал к программе dig, то мы увидим что есть опция +short, которая выписывает лишь ip адреса, без лишней информации. Так, мы можем сократить команду, всего до:

dig +short {www.,}$1 A | tee "$f_out"

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

Теперь можем перейти к 3-ей части, начнем с первых команд:

dig $1 NS | grep "^$1" | awk {'print $NF}'

Снова используя параметр +short, получится:

dig +short $1 NS

Далее, результат с этой команды посылается в цикл while:

dig +short $1 NS | while read nsserv
do
nsname=${nsserv:0:${#nsserv}-1}
echo "=================================="
echo "NS: $nsname"
dig @$nsname $1 A | grep "^$1" | grep -o -E "$ip_re" | tee -a $f_out
done

Думаю сразу видно, что к последней команде в цикле можно применить опцию +short:

dig +short @$nsname $1 A | tee -a $f_out

Так мы опять избавились от лишних действий.


Но вернемся к перемененной nsname, тут автор хочет удалить точку на конце переменной. В коде это было сделано без внешних программ(sed,awk,...), что очень хорошо, но можно ещё проще:

nsname=${nsserv:0:-1}

Подробнее о всех возможностях работы со значениями переменных, можете прочитать на bash-hackers.org.

В 4-ой части кода, есть не очень красивые решения, которые связаны с временными файлами, мы исправим это во время избавления от временных файлов. Так же не понятно, зачем в sort был использован параметр -h(human-numeric-sort). Т.к. sort использован только для дальнейшего uniq, то можно использовать sort  без параметров. Но мы будем использовать более правильную сортировку ip адресов:

sort -n -t . -k 1,1 -k 2,2 -k 3,3 -k 4,4 $f_out | uniq > $f_tmp

В данном случае, ip адреса будут отсортированы по числовым значениям каждого столбца.

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


Переменные o_re и ip_re, нам уже ни к чему, поэтому можем их удалить.


После всех правок код выглядит следующим образом:

#!/bin/bash


####PART 1####

if [[ -z "$1" ]]; then

echo "Error: missing argument" 1>&2 ; exit 1

fi


f_out="$(mktemp)"

f_tmp="$(mktemp)"


trap "rm -f $f_out $f_tmp" EXIT


####PART 2####

echo "*********************************************"

echo "Get A records from DNS:"


dig +short {www.,}$1 A | tee "$f_out"


####PART 3####

echo "*********************************************"

echo "Get NS records from DNS:"

dig +short $1 NS | while read nsserv

do

nsname=${nsserv:0:-1}

echo "=================================="

echo "NS: $nsname"

dig +short @$nsname $1 A | tee -a $f_out

done


####PART 4####

echo "*********************************************"

echo "Resolve ip range from whois service:"

sort -n -t . -k 1,1 -k 2,2 -k 3,3 -k 4,4 $f_out | uniq > $f_tmp

rm $f_out

cat $f_tmp | while read ip

do

echo "Get ip range for $ip"

whois $ip | grep -E -i "inetnum|route|netrange|cidr" >> $f_out

done


####PART 5####

echo "*********************************************"

echo "Result"

echo "*********************************************"

sort $f_out | uniq | while read range

do

echo "${range:16}"

done


В следующей(заключительной) части, мы избавимся от временных файлов и немного поработаем с выводом. А пока подумайте, почему лучше не использовать echo в скриптах? И ради примера, есть задачка на echo в комментариях.


Спасибо за внимание, надеюсь данный пост окажется для вас полезным.


P.s. если был непонятен как-то шаг или конструкция в коде, то жду ваших вопросов в комментариях.

Показать полностью
40

Разбор скрипта для командной строки Linux. Часть 1

Недавно в сообществе GNU/Linux появился пост с программой для Shell-а от PetRiot. В комментариях началось обсуждение целесообразности самой программы, но этот вопрос мне не интересен в полной мере, а вот качество самого кода хотелось бы улучшить. Поэтому я решил проанализировать код скрипта и дать пару советов как можно улучшить и сократить код. Надеюсь данный разбор будет полезен как для автора скрипта, так и для остальных читателей.

Начнем с первой строчки:

#!/bin/sh

Shell(sh) - самый старый интерпретатор командной строки, увы у него отсутствует множество возможностей, поэтому лучше использовать /bin/bash.


Данный код работает и в sh, но в дальнейшем мы будем использовать bash:

#!/bin/bash

Далее идет создание(запись названия файлов в переменные) временных файлов:

f_out=.get_ip_ranges
f_tmp=.ips

О том, что эти файлы временные, нам говорит то, что в конце их удаляют:

rm $f_out
rm $f_tmp

Тут сразу бы хотелось отметить несколько вещей:

1) создание временных файлов не всегда хорошо само по себе

2) названия могут конфликтовать с другими файлами, так мы можем случайно затереть важный файл с таким же названием

3) удаление временных файлов в конце кода. В случае ошибки удаление может не сработать(для данного случая маловероятно, но мы же стремимся к хорошему коду)


Первый пункт слишком спорный, поэтому я его проигнорирую.

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


Для решения второго пункта, я бы посоветовал использовать команду mktemp:

f_out="$(mktemp)"
f_tmp="$(mktemp)"

Теперь временные файлы однозначно уникальные, но нужно обеспечить их удаление в конце работы программы(в конце работы программы != в конце кода программы).


Для решения 3 пункта, используем команду trap:

trap "rm -f $f_out $f_tmp" EXIT

Команда trap запустит посланный ей код сразу после завершения программы, тем самым мы можем быть уверенны что файлы будут удалены. Также мы добавили параметр -f команде rm, который нужен для игнорирования ошибок и убирает вопросы о удалении.

Перед тем как перейти к следующей части, хотелось бы добавить ещё одну важную деталь в скрипт - проверку входных параметров.


Автор забыл упомянуть, что для запуска скрипта нужно обязательно послать один аргумент - название сайта. Если этого не сделать, то работа программы будет не очевидна. Добавим в начало проверку:

if [ -z "$1" ]; then
echo "Error: missing argument" 1>&2 ; exit 1
fi

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

Теперь начало кода выглядит так:

#!/bin/bash


if [ -z "$1" ]; then
echo "Error: missing argument" 1>&2 ; exit 1
fi


f_out="$(mktemp)"
f_tmp="$(mktemp)"
trap "rm -f $f_out $f_tmp" EXIT

Идем дальше:

dig $1 A | grep "^$1" | grep -o -E "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"

dig $1 A | grep "^$1" | grep -o -E "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" > $f_out
dig $1 A | grep "^www.$1" | grep -o -E "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"
dig $1 A | grep "^www.$1" | grep -o -E "(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)" >> $f_out

Тут начинаются любимые всеми регексы. Увы читабильность кода плохая, поэтому попробуем это исправить:

# Octet regex

o_re="(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"

# IP regex

ip_re="$o_re\.$o_re\.$o_re\.$o_re"


dig $1 A | grep "^$1" | grep -o -E "$ip_re"

dig $1 A | grep "^$1" | grep -o -E "$ip_re" > $f_out

dig $1 A | grep "^www.$1" | grep -o -E "$ip_re"

dig $1 A | grep "^www.$1" | grep -o -E "$ip_re" >> $f_out

Мы создали переменную o_re где находится повторяющая часть ip_re регекса и саму переменную ip_re.


Теперь код короче, но можно сократить ещё:

# Octet regex

o_re="(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"
# IP regex
ip_re="$o_re\.$o_re\.$o_re\.$o_re"


dig $1 A | grep "^$1" | grep -o -E "$ip_re" | tee "$f_out"
dig $1 A | grep "^www.$1" | grep -o -E "$ip_re" | tee -a "$f_out"

Используя команду tee мы выписали результат команды в командную строку и одновременно в файл $f_out.

Исправив с учетом этого остальную часть кода и получим:

#!/bin/bash


if [ -z "$1" ]; then

echo "Error: missing argument" 1>&2 ; exit 1

fi


f_out="$(mktemp)"

f_tmp="$(mktemp)"


trap "rm -f $f_out $f_tmp" EXIT


echo "*********************************************"

echo "Get A records from DNS:"

# Octet regex

o_re="(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)"

# IP regex

ip_re="$o_re\.$o_re\.$o_re\.$o_re"


dig $1 A | grep "^$1" | grep -o -E "$ip_re" | tee "$f_out"

dig $1 A | grep "^www.$1" | grep -o -E "$ip_re" | tee -a "$f_out"


echo "*********************************************"

echo "Get NS records from DNS:"

dig $1 NS | grep "^$1" | awk {'print $NF}' | while read nsserv

do

nsname=${nsserv:0:${#nsserv}-1}

echo "=================================="

echo "NS: $nsname"

dig @$nsname $1 A | grep "^$1" | grep -o -E "$ip_re" | tee -a $f_out

done


echo "*********************************************"

echo "Resolve ip range from whois service:"

sort -h $f_out | uniq > $f_tmp

rm $f_out

cat $f_tmp | while read ip

do

echo "Get ip range for $ip"

whois $ip | grep -E -i "inetnum|route|netrange|cidr" >> $f_out

done


echo "*********************************************"

echo "Result"

echo "*********************************************"

sort $f_out | uniq | while read range

do

echo "${range:16}"

done


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


Спасибо за внимание, надеюсь данный пост окажется для вас полезным.

P.s. прошу прощение за орфографические ошибки, мне бы точно не помешал разбор моего текста с орфографической точки зрения :)

Показать полностью
Отличная работа, все прочитано!