Генерация списка IPv4 адресов на TCL и немного систем счисления

    Не так давно потребовалось решать задачу массового обновления конфигурации устройств. Стандартная задача системного администрирования, если у вас в обслуживании больше одного устройства выполняющего однотипные функции. Для решения существуют как универсальные продукты, например из доступных redmine.nocproject.org, так и множество скриптов широко представленных на тематических форумах и порталах. Как раз на этот случай под рукой и должен был оказаться собственный написанный скрипт, но не оказался, поэтому учитывая что время для манёвров было, скрипт был написан заново, выполнен и положен на полочку, чтобы там в очередной раз затеряться.
    Для написания был использован expect — expect.sourceforge.net, надстройка над TCL позволяющая обрабатывать и реагировать на ответы различных интерактивных консольных утилит, в частности, telnet. Учитывая что для TCL раньше писать не приходилось, код нуждался в повторном осмыслении. Ключевой момент скрипта это генератор списка IPv4 адресов для обработки, после внимательной оценки данный кусок программы удалось значительно, на мой взгляд, оптимизировать, по крайней мере сократить количество строк на треть и безболезненно добавить новый функционал. Причём все эти сокращения мало относились к специфики TCL, а касались принципиальных подходов к построению алгоритма в целом.
    Я выделил этот код в отдельную утилиту, которую попытаюсь очень подробно разобрать далее по тексту — как было «до» и что стало «после», и почему не получилось написать сразу так как «после». Мне не всё в ней нравится до сих пор: смущают как алгоритмические проблемы так и проблемы TCL, например использование списков вместо массивов (что быстрее?, безопаснее?, идеологически вернее?), все сомнения тоже присутствуют в тексте, с надеждой на конструктивные комментарии.
    Логика работы утилиты (cipl.tl) следующая — в командной строке мы задаём два параметра: IPv4 адрес с которого будем начинать строить наш список и IPv4 адрес которым список закончится или число показывающее сколько элементов в списке должно быть. Порядок построения от меньшего адреса (первый параметр) к большему. Если второй параметр опущен то выводится список состоящий только из начального IPv4 адреса:
    > cipl.tl 192.0.2.1 1
    192.0.2.1
    192.0.2.2
    > cipl.tl 192.0.2.1 192.0.2.2
    192.0.2.1
    192.0.2.2
    > cipl.tl 192.0.2.1
    192.0.2.1
    

    Для Windows скрипт запускается вместе с интерпретатором tclsh, собственно тоже можно делать и в *nix
    > tclsh cipl.tl 192.0.2.1
    192.0.2.1
    

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

    Вер.1, №1-12
    #!/usr/bin/tclsh8.5
    
    set exip {^(2(5[0-5]|[0-4]\d)|1\d{2}|[1-9]\d{0,1})(\.(2(5[0-5]|[0-4]\d)|1\d{2}|[1-9]\d|\d)){3}$}
    set exdg {^(0?|([1-9]\d{0,5}))$}
    
    if {$argc == 0 || $argc > 2} then {
            puts "cipl.tl - Выводит список IP адресов начиная с заданного"
            puts "Использование: cipl.tl <start IP> \[<count>|<finish IP>\]"
    	puts "Аргументы: <start IP>  - IPv4 адрес меньший либо равный <finish IP>"
    	puts "\t   <finish IP> - IPv4 адрес"
    	puts "\t   <count>     - число от 0 до 999999"
    } else {	
    

    В первой строчке показываем какой нам надо использовать интерпретатор, это строчка для Linux. В общем случае надо указать полный путь до tclsh, например для FreeBSD эта строчка будет иметь вид:
    #!/usr/local/bin/tclsh8.5

    Дальше задаём переменные exip и exdg представляющие собой регулярные выражения которые будем использовать дальше в программе. Первая переменная нам нужна для проверки правильности ввода IPv4 адреса. Под данное регулярное выражение попадают правильные адреса написанные в десятичной форме от 1.0.0.0 до 255.255.255.255, то есть задать адрес вида 192.0.02.010 не получится. Вторая переменная определяет число, без ведущих нулей — допустимые границы списка от 0 до 999999, пустая строка также является верной. Ограничение сверху 999999, на мой взгляд, было разумным и кроме того я не стал тратить время на поиск регулярного выражения соответствующего числу 2 в степени 32. Данные регулярные выражения появились не сразу, а были добавлены исходя из потребностей решения, что и объясняет почем они именно такие, но это будет видно чуть дальше.
    Следом проверяется условие if — количество переданных параметров из командной строки, если их 0 или больше 2 то выводим небольшую справку. В этом месте можно ничего и не выводить, а просто принять первые два параметра за искомые, тем самым больше сконцентрироваться на пакетной работе утилиты без загромождающего вывода в случае ошибки.
    Последняя строчка открывает собой блок else в котором и происходит основная обработка.

    Вер.1, №12-17
    } else {	
    	set startip [lindex $argv 0]
    	set countip [lindex $argv 1]
    	set getcountip $countip	
    	if {[regexp $exip $startip]} then {
    		set octsip [split $startip {.}]
    

    В этой части мы сначала сохраняем параметры полученные на входе в переменные startip — первый параметр, countip — второй параметр, если второго параметра нету то lindex вернёт нам пустую строку. А также сохраняем второй параметр в дополнительной переменной getcountip.
    Далее мы проверяем соответствие первого параметра правильному IP адресу с использованием regexp и переменной exip с регулярным выражением заданной ранее. В этом условии важно что IPv4 адрес полностью соответствует тому что мы ожидаем, так как следующей строчкой мы создаём список octsip при помощи split, в качестве разделителя выступает символ точки. Получившийся список обязан содержать только десятичные цифры от 0 до 255 в нужных местах без ведущих нулей, чтобы оперировать с ними без дальнейших дополнительных проверок. Ведущие нули здесь играют роль, постольку поскольку, например, число 011 при подстановке будет восприниматься восьмеричным числом, то есть будет равно 9 в десятичной системе счисления.
    Здесь стоит обратить внимание на то, что запрос в поисковике довольно часто приводит к регулярным выражениям которые всех этих условий не проверяют, часто это просто проверка на 4 группы по 3 цифры в группе. Например выражение из habrahabr.ru/blogs/webdev/123845 — позволяет конструкции 000.100.1.010, что конечно является IP адресом, но не определяет однозначно форму его записи восьмеричная или десятичная, это вносит неопределённость и требует дальнейших проверок.

    Вер.1, №18-35
    if {[regexp $exip $countip]} then {
    	set octfip [split $countip {.}]			
    	set octsub {0}			
    	set countip {0}
    	for {set i 3} {$i>=0} {incr i -1} {
    		if {[set octsub [expr [lindex $octfip $i] - [lindex $octsip $i]]] < 0} then {
    		    if {$i > 0} then {
    			  set si [expr $i - 1]
    			  set octfip [lreplace $octfip $si $si [expr [lindex $octfip $si] - 1]]
    			  set octsub [expr 256 + $octsub]
    		    } else {						
    			  break
    			}	
    		}
    		set ni [expr 3 - $i]
    		set countip [expr $countip + ($octsub * (1 << ($ni * 8)))] 
    	}
    }
    

    Здесь мы проверяем является ли второй параметр верным IPv4 адресом и если это так, то пытаемся вычислить разность между этим адресом и тем который задан в первом параметре, то есть на выходе из этого блока, переменная countip должна содержать верное значение длины списка. Проверка условия if вложена в вышестоящую проверку (16 строка), таким образом если предыдущая проверка провалена (первый параметр не IPv4 адрес) то до этого участка выполнение программы не дойдёт.
    Решение этой подзадачи (вычитание IPv4 адресов) сделано так как если бы мы вычитали в столбик два числа:
     192.168.3.  1
    -192.168.2. 10
    =  0.  0.0.247
    

    Конечно это числа не десятичные, а по основанию 256. То есть когда мы занимаем значение из предыдущего разряда мы должны прибавить не 10, а 256 (0x100) в шестнадцатеричном представлении октетов это выглядит нагляднее:
     0xC0.0xA8.0x03.0x01
    -0xC0.0xA8.0x02.0x0A
    ---------------------- (выполняем заём, убираем единицу из старшего разряда)
    =0xC0.0xA8.0x03.0x01
             -.0x01
    ---------------------- (продолжаем заём, корректируем результат на величину основания системы счисления)
    -0xC0.0xA8.0x02.0x00A
                  +.0x100
    ---------------------- (в итоге получается такая операция)
    =0xC0.0xA8.0x02.0x101
    -0xC0.0xA8.0x02.0x00A
    =0x00.0x00.0x00.0x0F7
    

    Для реализации этого, мы также как и с первым параметром, формируем список otcfip, далее создаём переменную octsub в которой будет содержаться разница между октетами и обнуляем countip в которой будет храниться размер списка адресов, а не IPv4 адрес который там был, когда мы вошли в этот блок кода.
    Организуем цикл for с переменной i в обратном порядке от 3 до 0. В этом цикле мы должны пройти все октеты IPv4 адреса начиная с младшего, то есть все элементы списков octsip и octfip начиная с последнего элемента.
    Вычисляем разницу между текущими октетами octfip-octcip и сохраняем её в octsub. Здесь очень хотелось использовать массивы, потому что конструкция с lindex очень громоздкая, но простой способ сформировать массив (в TCL он только ассоциативный) из списка я не увидел, поэтому везде присутствуют только списки. Вычисленная разница тут же проверяется на условие меньше 0, то есть надо нам делать заём из старшего октета или нет?
    Если заём делать надо и это не самый старший октет (i большее 0), тогда к разнице octsub добавляем 256 и из следующего октета уменьшаемого (octfip) вычитаем 1.
    Если это самый старший октет, тогда у нас уменьшаемое octfip меньше вычитаемого octsip, то есть разность получается отрицательной, что не может быть по условию задачи, в этом случае мы выходим из цикла — break
    Если заём делать не надо, то значит результат нас удовлетворяет. Однако результат в любом случае представлен в системе счисления с основанием 256, что не является удобным, так как дальнейшие вычисления нам надо делать в системе которую понимает интерпретатор. Поэтому переводим получившейся результат стандартным способом для позиционной системы счисления:
    ...+Ai*Bi...+A3*B3+A2*B2+A1*B1+A0*B0=NB, где B основание системы счисления.
    В нашем случае: countip=octsubni*256ni, где ni меняется от 0 до 3, или ni=3-i, где i меняется от 3 до 0, что позволяет включить перевод в существующий цикл. Так как основание системы счисления у нас кратно 2 то для вычисления степени мы используем сдвиг, кратный 8, так как 256 это 100000000 в двоичном представлении, то есть единица сдвинутая на 8 бит влево. Таким образом сдвигая сначала на 0, потом на 8 (последняя значимая строчка на этом участке кода), потом на 16 и на 24, мы тем самым умножаем на 1(2560), 256, 2562 и 2563.
    Вернувшись к этому участку во второй раз, он у меня вызвал некоторое недоумение, что казалось просто и понятно при реализации, сейчас казалось излишне запутанно и усложнено. Об этом можно судить даже по количеству текста описывающий этот код.
    Что же не так? Зачем понадобилось, заново!, придумывать операцию вычитания, пусть и для чисел в системе счисления с основанием 256, вместо того чтобы перевести эти значения в понятный языку программирования числовой вид, и уже стандартными средствами сделать вычитание, тем более что перевод мы всё равно производим? В конечном итоге, для себя я пришёл к выводу, что субъективное человеческое восприятие сыграло очередной раз злую шутку. Что может быть проще выполнения действий в столбик, которое проходят в первом классе? Ничего, потому что это первое чему всех учат после собственно чисел. Переводы, сдвиги, кажутся сложными, по сравнению с простейшей операцией из первого класса. Понимание что это не десятичная система счисления приходит чуть позже, а понимание неправильности происходящего, лишь когда пришлось взглянуть на написанный код второй раз. В итоге второй вариант того же участка.

    Вер.2, №18-25
    if {[regexp $exip $countip]} then {
    	set octfip [split $countip {.}]
    	set nfip [expr ([lindex $octfip 0] * 0x1000000) + ([lindex $octfip 1] * 0x10000)\
     			+ ([lindex $octfip 2] * 0x100) + ([lindex $octfip 3])]
    	set nsip [expr ([lindex $octsip 0] * 0x1000000) + ([lindex $octsip 1] * 0x10000)\
     			+ ([lindex $octsip 2] * 0x100) + ([lindex $octsip 3])]	
    	if {$nfip >= $nsip} then {set countip [expr $nfip - $nsip]}
    }
    

    После формирования списка octfip (также как и в первом варианте), мы формируем числа соответствующие значению IPv4 адреса (коими они и являются) в переменной nfip — для адреса во втором аргументе и переменной nsip — для адреса в первом аргументе. Перевод делаем также как и делали, только без всяких циклов, подставив значения в одну строчку: nfip=listitem0*2563+listitem1*2562+listitem2*256+listitem3, где listitemn, соответствующий элемент списка, который вчисляем прямо в выражении с помощью lindex. 256 в какой-то степени, в коде для простоты восприятия представлено в виде круглых шестнадцатеричных значений 0x100xxxx. Потом проверяем, что второй аргумент больше первого и вычитаем из второго первый сохраняя значение результата в countip.
    В итоге, получилось немного проще чем было, даже сильно проще. Единственная вещь которая меня смущает в этом варианте, это гипотетическая возможность переполнения переменных nfip и nsip при вычислениях expr. Хотя для актуальных компиляторов Си это не должно быть страшно. Из документации по поводу вычислений и переполнений http://www.tcl.tk/man/tcl8.5/TclCmd/expr.htm#M23. Для версии 8.4 www.tcl.tk/man/tcl8.4/TclCmd/expr.htm#M5 явно указывалось что числовые константы это 32-х битные знаковые числа, которые при необходимости будут восприниматься как 64 битные знаковые, для версии 8.5 этого упоминания нету. В предыдущем варианте гипотетическая возможность переполнения тоже присутствовала, но там мы обрабатывали уже получившуюся разницу, которая в реальных случаях применения была бы сильно меньше даже 16 битного числа.
    Далее начинается вторая часть утилиты, в которой формируется выводимый список IPv4 адресов.

    Вер.2, №26-27
    if {[regexp $exdg $countip]} then {
    	puts $startip
    

    Проверяем переменную countip на соответствие числовому значению от 0 до 999999. Значение этой переменной может быть переданным во втором аргументе, то есть предыдущая проверка её на принадлежность к IPv4 адресу провалилась. Либо уже высчитанной разницей между заданными в аргументах адресами. Если значение этой переменной слишком велико, либо вообще не соответствует числу (такое может быть уже после наших вычислений, например, если разность IPv4 адресов отрицательна) то дальнейшая обработка производится не будет. Если же всё в порядке, то выводим первый элемент из списка (IPv4 адрес заданный первым аргументом). Дальше я буду называть получаемый список IPv4 адресов последовательностью, чтобы не путаться с внутренним понятием TCL — список.

    Вер.2, №28-29
    for {set i 0} {$i<$countip} {incr i 1} {
    	set octsip [lreplace $octsip {3} {3} [expr [lindex $octsip {3}] + 1]] 
    

    Формируем остальные элементы искомой последовательности, опять же очень хочется использовать массивы, но перевод из списка в массив мне кажется хуже, чем использование списков в таком виде (как это сделать правильно и просто?). Здесь цикл for для переменной i пробегающей значения от 0 до максимального высчитанного (или заданного) элемента последовательности countip. Внутри цикла последний элемент ранее сформированного списка octsip (младший октет в нашем адресе) увеличиваем на 1…

    Вер.2, №30-36
    for {set j 3} {$j>=0} {incr j -1} {
    	if {[lindex $octsip $j] > 255 && $j > 0} then {
    		set sj [expr $j - 1]
    		set octsip [lreplace $octsip $j $j {0}]
    		set octsip [lreplace $octsip $sj $sj [expr [lindex $octsip $sj] + 1]]
    	}
    }
    

    … и проверяем не нужно ли корректировать другие разряды. Для чего также организуем цикл for с переменной j пробегающей значения от 3 до 0. Далее в условии if делаем проверку на то что текущий октет больше 255 (произошло переполнение) и это не старший октет j больше 0, но не равна 0. Если переполнение произошло, текущий октет обнуляем, в старший октет (что соответствует элементу списка octsip ближе к его началу) добавляем 1. В случае если переполнение произошло в старшем октете, то корректировку не делаем, таким образом чтобы у нас остался неправильный IPv4 адрес.

    Вер.2, №37-44
    	set oip [join $octsip {.}]
    	if {[regexp $exip $oip]} then {
    		puts $oip
            } else {
    		puts "СТОП: Достигнут максимальный возможный адрес"
    		exit 3
    	}
    }
    

    Сливаем получившийся список содержащий октеты нашего адреса вместе, join в переменную oip разделитель — точка. Дальше проверяем результат на принадлежность к IPv4 адресу, с помощью нашего регулярного выражения заданного в самом начале. Если всё верно — выводим, если нет то произошло переполнение или другая ошибка уже в процессе формирования последовательности, аварийно выходим exit. Этот момент тоже не совсем красив, так как у нас образуется несколько точек выхода, что может быть неудобным если мы хотим, например, выполнять однотипные действия в конце.
    Последняя закрывающая скобка это завершение цикла for, формирующего выводимую последовательность и открытого в 28 строке.

    Вер.2, №45-51
    		} else {
    			puts "Неверно задан второй аргумент \"$getcountip\""			
    		}
    	} else {
    		puts "Неверно задан начальный IP адрес \"$startip\""
    	}
    }
    

    Завершающие строчки, в которых выводим сообщения об ошибках по веткам else для условий из 26 и 16 строк, где мы проверяем на соответствие ожиданиям заданные при старте программы аргументы. Это единственное место где используется переменная getcountip хранящая второй полученный аргумент программы в неизменном виде, что странно и кажется перебором, но очевидный (простой) другой вариант реализовать в этом случае не получалось.
    Просматривая второй раз уже эту часть программы (где формируется последовательность для вывода), я сначала подумал что неплохо было бы реализовать полный сумматор 4-х разрядных чисел в системе счисления по основанию 256 и транслятор в дополнительный код этих же самых чисел, чтобы можно было делать вычитание на том же сумматоре. Первую часть я к тому времени ещё не изменил, и был во власти представлений о простоте вычислений столбиком. Желание реализации данной (дикой) затеи не прошло, так как это интересно само по себе, но возможно не на TCL. Было уже ясно, что вторую часть надо изменить в том же ключе как и первую, то есть производить перевод из обычного представления в нужное нам (а это уже перевод в 256-ричную систему счисления).
    Поменялась и концепция перебора, если IPv4 адреса мы можем средствами языка перебирать в цикле for то нам не зачем заранее вычислять размер данной последовательность, мы просто будем двигаться от одного адреса подряд к другому. Также в данном подходе нам оказалось очень просто двигаться не только в прямом направлении от меньшего к большему, но и в обратном — дополнительных усилий это не требует, надо только правильно задать приращение переменной цикла при его формировании (тут появилась возможность для дополнительного функционала, который позволяет формировать последовательность в любом направлении).

    Вер.3, №31-32
    if {$nfip > $minip && $nfip < $maxip} then {
    			if {[set d [expr $nfip >= $nsip]]} then {set di {1}} else {set di {-1}}
    

    Проверяем принадлежность nfip, в которой, напомню, содержится второй аргумент IPv4 адрес как число, заданному диапазону (minip и maxip определяем в начале программы). Если в диапазон попадаем, далее устанавливаем направление перебора, если второй IPv4 адрес nfip больше первого nsip (адрес у нас уже в виде чисел), то перебор в прямом порядке — переменная di=1, если меньше, то перебор в обратном порядке, di=-1. Результат сравнения также запомнили в d.

    Вер.3, №33-37
    for {set i $nsip} {($i<=$nfip && $d) || ($i>=$nfip && !$d)} {incr i $di} {
    	set octip [list [expr ($i & 0xFF000000) >> 24] [expr ($i & 0xFF0000) >> 16]\
     		[expr ($i & 0xFF00) >> 8] [expr ($i & 0xFF)]]				
    	puts [join $octip {.}]				
    }
    

    Организуем цикл for по переменной i начальное значение которой установлено равным nsip, а условие выхода в корректируем с условием nfip>=nsip, результат которого мы храним в d: i<=nfip, если приближаемся к nfip снизу, или иначе i>=nfip. Приращение i уже вычислено и хранится в di.
    В теле цикла мы формируем список octsip из октетов IPv4 адреса. То есть нам надо сформировать адрес в десятичном представлении из его числового представления — перевести в 256-ричную систему счисления. В общем случае, следуя теории, нам надо делить число в одной системе счисления, на основание другой системы счисления и из остатков формировать число в новой системе счисления (на основание которой мы делим):
      3 221 225 985 | 256
     -3 221 225 984 | -----------
     -------------- |  12 582 914 | 256
                  1 | -12 582 912 | ------- 
                       ---------- |  49 152 | 256
                                2 | -49 152 | ---
                                          0 | 192                                                     
    

    Начиная от результата 192, по всем остаткам в обратном порядке 0, 2, 1 получаем 192.0.2.1. Деление сложная операция и не приносит никакой оптимизации, но в нашем, очень частном, случае: IPv4 адрес и деления на 256 — всё получается очень просто. Мы будем сдвигать на 8 (делить на степени 256) и маскировать не нужные нам разряды (бинарная операция «И»). Представим в шестнадцатеричном виде:
       0xC0000201       |  0xC0000201         |  0xC0000201        |  0xC0000201
      &0xFF000000       | &0x00FF0000         | &0x0000FF00        | &0x000000FF
     ---------------------------------------------------------------------
       0xC0000000 >> 24 |  0x00000000 >> 16   |  0x00000200 >> 8   |  0x00000001
     ---------------------------------------------------------------------
     = 0xC0 (192)       | = 0x00 (0)          | = 0x02 (2)         | = 0x01 (1)
    

    Всё это выполняется в одну строчку, каждый разряд помещается в свой элемент списка list. Второй оператор в теле цикла, выводит объединённый список.

    Вер.3, №38-45
    		} else {
    			puts "Последний IP списка выходит за границы допустимого диапазона"
    			exit 3
    		}
    	} else {
    		puts "Неверно задан начальный IP адрес \"$startip\""
    	}
    }		
    

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

    Вер.3, №1-7
    #!/usr/bin/tclsh8.5
    
    set exip {^(2(5[0-5]|[0-4]\d)|1\d{2}|[1-9]\d{0,1})(\.(2(5[0-5]|[0-4]\d)|1\d{2}|[1-9]\d|\d)){3}$}
    set exdg {^-?(0?|([1-9]\d*))$}
    set maxip {0xFFFFFFFF}
    set minip {0xFFFFFF}
    
    

    Регулярное выражение для проверки числового параметра теперь возвращает положительный ответ для любых числовых значений любой длины, без ведущего 0, но с возможным знаком отрицания "-" впереди. Здесь мы упростили проверку и расширили границы, потому что длина получающейся последовательности проверяется в числовом виде с помощью следующих переменных maxip и minip. Эти значения не дублируют регулярное выражение exip, так как оно проверяет теперь лишь корректность пользовательского ввода, а не результаты вычислений.

    Вер.3, №15-20
    set startip [lindex $argv 0]
    if {![string length [set finiship [lindex $argv 1]]]} then {set finiship {0}}	
    if {[regexp $exip $startip]} then {
    	set octsip [split $startip {.}]
    	set nsip [expr ([lindex $octsip 0] * 0x1000000) + ([lindex $octsip 1] * 0x10000)\
     		+ ([lindex $octsip 2] * 0x100) + ([lindex $octsip 3])]
    

    Строчки 8-14 почти полностью повторяют строчки 6-12 первого варианта, лишь немного поправили сообщения в соответствии с новым функционалом. Дальше мы выполняем почти те же действия что и по второму варианту. Единственное мы принудительно задаём значение finiship равным 0, если второй аргумент не был задан, чтобы переменная всегда была определена. finiship имеет тот же смысл что и countip из второго варианта, и была переименована чтобы соответствовать новой концепции. В конечном итоге эта переменна будет содержать не размер последовательности IPv4 адресов, а последний адрес из этой последовательности. Вычисление nsip производим сразу после того как разложили первый аргумент на составляющие.

    Вер.3, №21-30
    if {[regexp $exip $finiship]} then {
    	set octfip [split $finiship {.}]
    	set nfip [expr ([lindex $octfip 0] * 0x1000000) + ([lindex $octfip 1] * 0x10000)\
     		+ ([lindex $octfip 2] * 0x100) + ([lindex $octfip 3])]			
    } elseif {[regexp $exdg $finiship] && [expr abs($finiship)] < $maxip} then {
    	set nfip [expr $nsip + $finiship]
    } else {
    	puts "Неверно задан второй аргумент \"$finiship\""
    	exit 5
    }		
    

    Первое условие такое же как и во втором варианте, упомянутое изменение — перенесли вычисление первого аргумента чуть выше по коду.
    Второе условие elseif это проверка второго аргумента на соответствие числовому значению, и эта проверка выполняется не в любом случае как в первых вариантах, а только если второй аргумент не является IPv4 адресом, так как дальнейшие вычисления производятся именно с адресами, а не с длиной последовательности. Если это число, то nfip вычисляется суммированием этого числа с nsip. Проверка на правильность полученного IPv4 адреса, будет сделана численно дальше по коду (что описано выше по тексту).
    Если второй аргумент не соответствует ни числу, ни IPv4 адресу — прерываем выполнение exit. Опять же получаем множественность точек выхода, причём в этом варианте их целых три. С этим можно побороться обернув всю программу в бесконечный цикл и в случае необходимости прерывая его break попадая в конец программы, но здесь это совсем не было нужно. Как упоминалось ранее, все проверки ошибок можно исключить, заменив их на действия по умолчанию, это больше подходит под командный режим работы. В этом варианте нет переменной getcountip — в сообщении об ошибке присутствует непосредственно полученный второй аргумент finiship, так как он не изменяется, а лишь используется по ходу работы.
    Логика работы утилиты (cipl.tl) расширилась, теперь IPv4 адрес с которого будем начинать не обязан быть меньше того IPv4 адреса которым мы закончим, список будет строится в любом случае от первого ко второму. Также можно вторым параметром задать отрицательно число, в этом случае список будет в обратном порядке.
    > cipl.tl 192.0.2.1 -1
    192.0.2.1
    192.0.2.0
    > cipl.tl 192.0.2.1 192.0.2.0
    192.0.2.1
    192.0.2.0
    

    Вот и все злоключения, описание сильно превысило сам код, а вся ситуация с первоначальным варантом это подтверждение топика habrahabr.ru/blogs/complete_code/135340, но в итоге получилось не совсем плохо, как говорится: «Семь раз отмерь, один отрежь», плюс работающая утилита.

    Варианты программ в полном объёме: cipl.zip
    Про системы счисления можно почитать на wikibooks — ru.wikibooks.org/wiki/Системы_счисления
    Всё что нужно знать про TCL есть на www.tcl.tk/doc и на wiki.tcl.tk
    Support the author
    Share post

    Similar posts

    Comments 21

      +2
      В общем случае надо указать полный путь до tclsh
      Можно, например, воспользоваться env, правда, не знаю, есть ли там подводные камни.
      #! /usr/bin/env tclsh


      Или, как чуть хитрей иногда делают:
      #!/bin/sh
      # DO NOT REMOVE THIS BACKSLASH -> \
           exec tclsh "$0" ${1+"$@"}
      


      А насчет считалки — можно же конвертить v4-адрес в int и работать уже с числом?
      Впрочем, тут я мало что понимаю, поэтому настаивать не буду
      proc ip2int ip {
           set res 0
           foreach i [split $ip .] {set res [expr {wide($res<<8 | $i)}]}
           set res
       }
      


      Кстати, для повышения читаемости проверку IP можно скоммуниздить, например, из eggdrop-скрипта.
      proc testip {ip} {
        set tmp [split $ip .]
        if {[llength $tmp] != 4} then {
          return 0
        }
        set index 0
        foreach i $tmp {
          if {(([regexp \[^0-9\] $i]) || ([string length $i] > 3) || \
               (($index == 3) && (($i > 254) || ($i < 1))) || \
               (($index <= 2) && (($i > 255) || ($i < 0))))} then {
            return 0
          }
          incr index
        }
        return 1
      }
      
        0
        «Хитрее» лучше не делать. Правильно — через /usr/bin/env.
          0
          Официальный Style Guide разрешает оба метода.
            0
            Он несколько устарел.
          0
          В считалке как раз и получилась работа с адресами как с числом, к этому всё и вели.
          0
          Эх, утомили.

          ping 127.1377 — совершенно валидный IPv4 адрес.
            0
            ping 2130707809 — тоже работает, машине всё равно, а вот человеку не очень понятно. Строгая типизация адреса, нужна больше как защита от дурака, а то с получившимся списком можно таких дел натворить.
            Да получилось длинновато, каюсь, искание мысли длиннее выражать чем саму мысль.
              0
              Нет, обязательно поддерживать все форматы адресов. Мне, например, удобнее писать 127.1, а не 127.0.0.1.
                0
                Справедливости ради и чтобы не повторятся в обсуждениях, вот тут много копий сломали habrahabr.ru/blogs/sysadm/69587
                Запись 127.1 не однозначна, на мой взгляд — так и тянет добавить 127.1/16 и читать этот адрес 127.1.0.0
                  0
                  Нельзя. 127.1/16 == 127.0.0.1/16. Никак иначе.
                  The address supplied in cp can have one of the following forms:
                  • a.b.c.d Each of the four numeric parts specifies a byte of the address; the bytes are assigned in left-to-right order to produce the binary address.
                  • a.b.c Parts a and b specify the first two bytes of the binary address. Part c is interpreted as a 16-bit value that defines the rightmost two bytes of the binary address. This notation is suitable for specifying (outmoded) Class B network addresses.
                  • a.b Part a specifies the first byte of the binary address. Part b is interpreted as a 24-bit value that defines the rightmost three bytes of the binary address. This notation is suitable for specifying (outmoded) Class C network addresses.
                  • a The value a is interpreted as a 32-bit value that is stored directly into the binary address without any byte rearrangement.

                  In all of the above forms, components of the dotted address can be specified in decimal, octal (with a leading 0), or hexadecimal, with a leading 0X). Addresses in any of these forms are collectively termed IPV4 numbers-and-dots notation. The form that uses exactly four decimal numbers is referred to as IPv4 dotted-decimal notation (or sometimes: IPv4 dotted-quad notation).

                    0
                    Полный текст это man 3 inet -> www.kernel.org/doc/man-pages/online/pages/man3/inet_makeaddr.3.html
                    Приведённый вами участок описывает inet_aton(), а далее по тексту:

                    CONFORMING TO
                    4.3BSD. inet_addr() and inet_ntoa() are specified in POSIX.1-2001.
                    inet_aton() is not specified in POSIX.1-2001, but is available on most
                    systems.

                    Обычная практика говорит что это будет работать почти всегда, но не абсолютно всегда. Причём inet_ntoa() преобразует в полную нотацию с 4-мя десятичными октетами и он описан в POSIX.

                    Я не зря привёл запись 127.1/16 == 127.1.0.0/16, это встречается довольно часто. На вскидку, juniper подразумевает именно в этом смысле: www.juniper.net/techpubs/software/junos-es/junos-es92/junos-es-swconfig-interfaces-and-routing/ipv4-addressing.html — в разделе IPv4 Variable-Length Subnet Masks есть запись вида 192.14.17/24, в смысле сети 192.14.17.0/24, а никак не адреса 192.14.0.17/24.
                      0
                      Ещё раз Вам говорю, если где-то эта запись воспринимается неверно, это означает только то, что она воспринимается неверно. Насчёт POSIX.1-2001 написано про inet_aton(), а не про поведение при разборе адресов. inet_addr() в POSIX есть, как здесь и сказано.
                        +1
                          0
                          Да смотрел невнимательно, в мире POSIX это действительно так.
                        +1
                        Не знаю, где это формализовано (наверное, нигде), но когда речь идёт не об IP-адресе, а о префиксе типа 172.1/16, то обычно считается, что перед слешем записана именно самая левая часть префикса, а нули справа опущены. Такая запись встречается и в документах RFC, и многое сетевое оборудование в конфигурации воспринимает, например, запись 192.168/16 именно как 192.168.0.0/16.
                          0
                          Это мало того, что формализовано, так это ещё и не может быть иначе, ибо такая схема сформировалась ещё в те времена, когда хостов было мало, а потому что-то вроде ping 42 было распространённым явлением. Т.е. потом в эту схему не ломая обратной совместимости затолкали классовую адресацию и всё такое.
                            0
                            Ещё раз обратите внимание на то, что я имею ввиду сокращения для префиксов, а не для адресов.
                              0
                              Почему так категорично? Иначе быть может — Cisco не воспринимает ничего кроме строгой десятичной записи разделённой точками. Причём запись с ведущими нулями воспринимается также десятичной, а не восьмеричной. Cisco не подчиняется POSIX, что вероятно, но подчиняется RFC, что тоже во многом вероятно.
                              На мой взгляд это правильно с точки зрения использования, чем меньше потенциальной возможности напортачить — тем лучше, плата — меньше гибкости.
                  0
                  Вы очень зря не используете процедуры, а также лепите exit в середину скрипта. При использовании процедур, как ни странно, вы получите большее быстродействие засчёт байткод-компиляции.
                    0
                    В тексте про exit я упоминал, жутко некрасиво. Про процедуры спасибо, не видел необходимости в достаточно коротком коде выделять процедуры, буду знать — обычно исходил из принципа: «Всё что повторяется два раза выделяем в подпрограмму».
                    +1
                    Ну и да, посмотрите на пакет ip из tcllib. Хотя он некорректно разбирает IPv4 в неканоническом представлении, он упрощает работу со многими вещами.

                    Only users with full accounts can post comments. Log in, please.