Running numbers

About a year ago, I wrote a post comparing jot and seq, two utilities for generating sequences of numbers. They differ in the ordering of their arguments and in some of the options available for formatting the numbers. The upshot was that I liked some parts of jot, some parts of seq, and some parts of neither. The unwritten conclusion of the post was that I should write my own sequence-generating command with arguments and options tuned to the way I think and work. I’ve finally gotten around to doing that.

Here’s the code for run, a simple Python script that does the same simple task that jot and seq do, but with arguments ordered in a way that makes sense to me.

python:
 1:  #!/usr/bin/env python
 2:  
 3:  import docopt
 4:  import codecs
 5:  
 6:  usage = '''Usage:
 7:  run [options] <stop>
 8:  run [options] <start> <stop>
 9:  run [options] <start> <stop> <step>
10:  
11:  Generate a run of integers or characters. Similar to jot and seq.
12:  
13:  Options:
14:    -f FFF   formatting string for number
15:    -s SSS   separator string
16:    -c       characters instead of integers
17:    -r       reverse the run
18:    -h       show this help message
19:  
20:  The run of numbers can be integers or reals, depending on the values of start, stop, and step. The defaults for both start and step are 1. If -c is used, then start and stop must both be given as characters and step (if given) is an integer.'''
21:  
22:  # The arguments for -f and -s come in as raw strings, but we
23:  # need to be able to interpret things like \t and \n as escape
24:  # sequences, not literals.
25:  def interpret(s):
26:    if s:
27:      return codecs.escape_decode(bytes(s, 'utf8'))[0].decode('utf8')
28:    else:
29:      return None
30:  
31:  # Handle the command line options and arguments.
32:  args = docopt.docopt(usage)
33:  fstring = interpret(args['-f']) or '{}'
34:  sep = interpret(args['-s']) or '\n'
35:  rev = args['-r']
36:  char = args['-c']
37:  step = int(args['<step>'] or 1)
38:  
39:  # The interpretation of start and stop depend on -c
40:  if char:
41:    start = ord(args['<start>'])
42:    stop = ord(args['<stop>'])
43:  else:
44:    start = int(args['<start>'] or 1)
45:    stop = int(args['<stop>'])
46:  
47:  # Generate the run as a list of integers.
48:  # Include stop if it fits the sequence.
49:  run = list(range(start, stop, step))
50:  if run[-1] + step == stop:
51:    run += [stop]
52:  
53:  # Convert to text
54:  if char:
55:    runText = [ fstring.format(chr(n)) for n in run ]
56:  else:
57:    runText = [ fstring.format(n) for n in run ]
58:  
59:  # Reverse the list if asked.
60:  if rev:
61:    runText.reverse()
62:    
63:  print(sep.join(runText))

As you can see, run takes one, two, or three arguments. If given one argument, it prints the integers from 1 to that number, e.g.,

run 5

produces

1
2
3
4
5

This is just like jot and seq with a single argument.

With two arguments, run prints the integers from the first through the second, e.g.,

run 4 9

produces

4
5
6
7
8
9

This is just like seq, but very different from jot, which would need

jot 6 4

to produce the same list of integers.

With three arguments, run prints the integers from the first through the second, stepping by the third, e.g.,

run 2 12 2

produces

2
4
6
8
10
12

The argument ordering of start stop step is unlike both jot and seq, which would need

jot - 2 12 2

and

seq 2 2 12

both of which I find difficult to remember.

Undoubtedly, I find start stop step easy to remember because that’s the ordering of arguments in Python’s range() function, which I use all the time. The difference between run and range is that run’s default starting point is 1 instead of 0, and it continues through to the second argument instead of stopping just short of it. In other words, it’s written for humans, not programmers.

As you can see, though, if start, stop, and step don’t align, run won’t generate the stop value. For example,

run 1 10 2

will only print

1
3
5
7
9

because 10 doesn’t fit in the sequence. This was, to me, the best way to interpret arguments that aren’t in sync with one another.

The -f option expects a formatting string in the style of Python’s format method. Again, this syntax is easy for me to remember because I use it frequently. I can, for example, generate a sequential list of parts like this:

run -f 'Part 52Q39-{:02d}' 8 13

gives

Part 52Q39-08
Part 52Q39-09
Part 52Q39-10
Part 52Q39-11
Part 52Q39-12
Part 52Q39-13

The -s option lets me specify the separator between items in the list:

run -s ', ' 5

produces

1, 2, 3, 4, 5

This is like jot’s -s option but unlike seq’s, which treats the argument of -s as a suffix, not a separator:

seq -s ', ' 5

gives

1, 2, 3, 4, 5, 

with the comma and space trailing after the last item, too. I don’t know why anyone would want this.

Like jot, but not seq, run can produce a sequence of characters:

run -c -f 'Apt. {}' A E

gives

Apt. A
Apt. B
Apt. C
Apt. D
Apt. E

The -r option for reversing the list isn’t necessary—I could always pipe the result through sort -r—but I need reversed lists often enough that it’s worth having built-in.

You’ll note that I still use docopt to deal with options and arguments. It’s a great library, so much nicer to use than the supposedly easy argparse. One oddity that came up in this script was that I couldn’t include the default values for -d and -s in the usage string. Normally, I’d do something like

  -s SSS   separator string [default: \n]

and docopt would automatically assign the default string to args['-s'] if run was called without an -s option. But writing it that way caused a linebreak to appear at that point in the help message, which I didn’t want. Even worse, escaping the backslash by writing it as

  -s SSS   separator string [default: \\n]

meant that the literal character pair \n would be the default separator, which was flatly wrong. So I left the defaults out of the usage string and handled them in Lines 23 and 24.

And speaking of escapes, the interpret function in Lines 25–29 is needed to handle escape codes like \n and \t in the format and separator strings. When docopt processes the options, it treats the arguments to -s and -f literally, which is not what I want. When I say

run -s '\t' 5

I want

1   2   3   4   5

with actual tab characters between the numbers, not

1\t2\t3\t4\t5

I learned how to use the codecs module to process escapes from this Stack Overflow page.

I’m not done with run. It doesn’t handle floating point numbers, and its error handling consists of passing Python’s errors along to the user. Since I’m the only user, this latter deficiency isn’t all that bothersome, but I suspect I’ll be wishing I could use it for fractional steps before too long. One of the nice things about homemade programs, though, is that they can grow with you. You get to tweak them as you see how they work—and don’t work—under real-world conditions.