Gnu seq on steroids with hex support and descending ranges

From Svendsen Tech PowerShell Wiki
Jump to: navigation, search




General Information

The idea came from the simple script I wrote an article about here, where I describe a primitive "hexadecimal seq version". With this script I made a more finished and polished product.

I decided to try to duplicate the functionality of the GNU utility seq, and add a few features, such as hexadecimal input and/or output, and descending ranges. Negative numbers are somewhat supported. I really wanted completely dynamic decimal support, but failed to implement it easily, so I gave it up for the time being.

Someone pointed out that this example with GNU seq's --format parameter and a custom, negative, decimal increment (decrement) value demonstrates a lot of what my seq version features:

$ seq --format '%05.2f' 2 -.5 -1
02.00
01.50
01.00
00.50
00.00
-0.50
-1.00

Parameters

--pad Pad with leading zeroes to make the number contain at least the specified number of digits.
--hexin Interpret specified FIRST and LAST numbers as hexadecimal.
--hexout Display output as hexadecimal values. The default is decimal, even when --hexin is specified.
--format A sprintf-type format to display numbers in. Overrides --pad and --equalwidth. Not in effect if using --hexout.
--equalwidth Pads with leading zeroes to make all numbers the same width, if necessary. Not in effect if using --hexout. Overrides --pad.
--increment Number that signifies the steps between numbers.
--separator Separator between elements. Default "\n" (a newline).
FIRST First decimal or hex value in range to enumerate. Can be larger than LAST to count down.
LAST Last decimal or hex value in range to enumerate. Can be smaller than FIRST to count down.

Examples

A few examples.

The Basics

$ ./st-seq.pl 8 13
8
9
10
11
12
13

Padding

$ ./st-seq.pl 8 13 --pad 3
008
009
010
011
012
013

Hexadecimal Output

Note that --format, --pad and --equalwidth are all ignored when you supply --hexout.

$ ./st-seq.pl 8 13 --hexout
8
9
a
b
c
d

Hexadecimal Input and Output

Even if you specify --hexin, output will be in decimal by default. If you specify --hexout, input will still be assumed to be decimal. To work entirely with hexadecimal values, you will need to use both the --hexin and --hexout parameters.

$ ./st-seq.pl aa bb --hexin --hexout --sep ' '
aa ab ac ad ae af b0 b1 b2 b3 b4 b5 b6 b7 b8 b9 ba bb

Descending Range

Just put the highest number first and the lower number last. And I tacked on an example of --equalwidth.

$ ./st-seq.pl 13 8 --equalwidth
13
12
11
10
09
08

Custom Increment Step

$ ./st-seq.pl 100 500 --increment 100
100
200
300
400
500

Custom Separator

Here I use ", " instead of the default newline as a separator (and pad with leading zeroes so the result is always at least 5 digits).

$ ./st-seq.pl 1017 1013 --pad 5 --separator ', '
01017, 01016, 01015, 01014, 01013

Descending Range with Custom Increment Step

$ ./st-seq.pl 1017 1004 --increment 2
1017
1015
1013
1011
1009
1007
1005

Negative Numbers

Do not use --equalwidth or --pad together with negative numbers as this causes an overflow and I just chalk it up as "undefined behavior".

You might need to use "--" after possibly passing other options so the negative numbers won't be interpreted as arguments. This is shell behavior.

$ ./st-seq.pl -- -5 -1
-5
-4
-3
-2
-1

Negative range and negative numbers:

$ ./st-seq.pl -- -1 -5
-1
-2
-3
-4
-5

Custom Formats

The formats you can use are the ones the sprintf function in Perl accepts. It's documented in perldoc -f sprintf.

$ ./st-seq.pl --format '%.2f' -- -1 -5
-1.00
-2.00
-3.00
-4.00
-5.00

Incrementing by a Decimal Value

$ ./st-seq.pl --increment 0.5 -- -2.5 1
-2.5
-2
-1.5
-1
-0.5
0
0.5
1
$ ./st-seq.pl --increment 0.4 -- -2.5 1
-2.5
-2.1
-1.7
-1.3
-0.9
-0.5
-0.1
0.3
0.7

Downloads

  • St-seq.txt - Perl code in a text file. Right-click and "save as".
  • St-seq.zip - A packaged, standalone Windows executable made from the source code.

Source Code

#!/usr/bin/perl

# Copyright (c) 2012, Svendsen Tech
# All rights reserved.
# 
# Author: Joakim Svendsen
# st-seq - seq on steroids

use warnings;
use strict;
use File::Basename;
use Getopt::Long;

our $VERSION = '1.02';

main();

sub main {
    
    my %opt;
    
    GetOptions( 'hexin'          => \$opt{hexin},
                'hexout'         => \$opt{hexout},
                'help'           => \$opt{help},
                'equalwidth'     => \$opt{equalwidth},
                'format=s'       => \$opt{format},
                'pad=i'          => \$opt{pad},
                'separator=s'    => \$opt{separator},
                'increment=s'    => \$opt{increment},
               ) or die "Unable to process arguments!\nError: $!";
    
    print_help() if $opt{help};
    
    die "Missing start and end decimal/hex values\nUse --help for help.\n" if @ARGV == 0;
    die "Missing end decimal/hex value\nUse --help for help.\n" if @ARGV == 1;
    
    my ($first_num, $second_num) = @ARGV;
    
    my $regex = $opt{hexin} ? qr'[^a-f0-9]' : qr'[^\d\-.]';
    
    die "Illegal characters or pattern found in first decimal/hex value\n"  if $first_num  =~ m/$regex/i;
    die "Illegal characters or pattern found in second decimal/hex value\n" if $second_num =~ m/$regex/i;
    
    my $format = $opt{format} ? $opt{format} : '%s';
    my $increment = $opt{increment} ? $opt{increment} : 1;
    
    # Get a "boolean" for whether the first number is the lowest.
    my $first_is_lower = get_lower($first_num, $second_num, $opt{hexin});
    
    # Set padding if --equalwidth or --pad are specified. Otherwise set to 0 (false).
    # --equalwidth overrides --pad.
    my $pad = 0;
    if ($opt{equalwidth}) {
        # If the first number is the lowest, the number containing the max
        # amount of digits must be the other one, and vice versa.
        $pad = $first_is_lower ? length $second_num : length $first_num;
    }
    elsif ($opt{pad}) {
        $pad = $opt{pad};
    }
    
    my @output_list = enumerate_range( $first_num, $second_num, $format, $increment, $pad, $opt{hexin}, $opt{hexout}, $first_is_lower );
    print join $opt{separator} ? $opt{separator} : "\n", @output_list;
    
    print "\n";
    
}


sub print_help {
    
    my $prog_name = basename $0;
    
    print <<EOF;
$prog_name [--pad] [--hexin] [--hexout] [--format <value>]
    [--equalwidth] [--increment <value>] [--separator <value>]
    [--help] FIRST LAST

--pad:         Pad with leading zeroes to make the number contain at least the
               specified number of digits.
--hexin:       Interpret specified FIRST and LAST numbers as hexadecimal.
--hexout:      Display output as hexadecimal values. The default is decimal,
               even when --hexin is specified.
--format:      A sprintf-type format to display numbers in. Overrides --pad
               and --equalwidth.
--equalwidth:  Pads with leading zeroes to make all numbers the same width,
               if necessary. Not in effect if using --hexout. Overrides --pad.
--increment:   Decimal that determines the steps between numbers.
--separator:   Separator between elements. Default "\\n".

FIRST:         The first number in the range to start counting up or down from.
               It can be higher than the LAST number to count down.
LAST:          The last number in the range to count up to or down towards,
               from FIRST.

Examples:
$prog_name --sep ', ' 5 1
$prog_name --pad 3 99 102
$prog_name --hexin --hexout 10 20
$prog_name --equalwidth 42 1004 --separator ' '
EOF
    
    exit;
    
}


sub get_lower {
    
    my ($first_number, $second_number, $hexin) = @_;
    
    if ($hexin) {
        
        return (hex $first_number < hex $second_number) ? 1 : 0;
            
    }
    
    return ($first_number < $second_number) ? 1 : 0;
    
}


sub enumerate_range {
    
    my ($start_num, $end_num, $format, $increment, $pad, $hex_in, $hex_out, $first_is_lower) = @_;
    
    my ($start_dec, $end_dec);
    
    if ($hex_in) {
        $start_dec = hex $start_num;
        $end_dec   = hex $end_num;
    }
    else {
        $start_dec = $start_num;
        $end_dec   = $end_num;
    }
    
    my @num_list;
    
    # Code duplication. Meh.
    if ($first_is_lower) {
        
        # Increment for each iteration when first is lower.
        for (my $decimal = $start_dec; $decimal <= $end_dec; $decimal += $increment) {
            
            if ($hex_out) {
                
                push @num_list, sprintf '%x', $decimal;
                
            }
            
            else {
                
                if ($format ne '%s') {
                push @num_list, sprintf $format, $decimal;
                }
                elsif ($pad) {
                    push @num_list, sprintf "%0${pad}d", $decimal;
                }
                else {
                    push @num_list, $decimal;
                }
                
            }
            
        } # end of for loop
        
    }
    
    # This is when the first number is higher than the last.
    else {
        
        # Decrement for each iteration when first is higher.
        for (my $decimal = $start_dec; $decimal >= $end_dec; $decimal -= $increment) {
            
            if ($hex_out) {
                
                push @num_list, sprintf '%x', $decimal;
                
            }
            
            else {
                
                if ($format ne '%s') {
                    push @num_list, sprintf $format, $decimal;
                }
                elsif ($pad) {
                    push @num_list, sprintf "%0${pad}d", $decimal;
                }
                else {
                    push @num_list, $decimal;
                }
                
            }
            
        } # end of for loop
        
    }
    
    return @num_list;
    
}