15

I've noticed you can't really count on seq(1) being available on anything but GNU systems. What's a simple reimplementation of seq(1) I can bring with me written in POSIX (not bash) shell?

EDIT: Note that I intend to use it on at least various BSD's, Solaris, and Mac OS X.

5
  • It might be easier to include a Solaris-compatible version if you can use a env sh-bang line that pulls in something other than /bin/sh
    – Jeff Schaller
    Jul 25, 2016 at 17:31
  • 1
    A little note: You need to be carefully with posix shell testing on Linux, because it can run some bash specific code even if you are using an ancient shell, because Linux have bash compatibility binaries present on /usr/bin, like /usr/bin/test and others, and your shell script cannot run in other systems with the exactly same shell installed! You think some command is accepted by your very specific interpreter, but it is not. Jul 25, 2016 at 17:39
  • @LucianoAndressMartini I usually test with the old Heirloom bourne shell. I'm not sure if there's things in POSIX not in this shell though, or vis versa.
    – Elizafox
    Jul 25, 2016 at 17:41
  • Mac OS X and FreeBSD also have seq(1). They seem to take the same arguments as the Linux version. Jul 25, 2016 at 20:23
  • 1
    @EdwardFalk Only as of FreeBSD 9.0 (I still have some 8.0 boxes), and Mac OS X 10.11.
    – Elizafox
    Jul 26, 2016 at 2:14

5 Answers 5

10

An alternative to awk is bc:

seq() (first=$1 incr=$2 last=$3
  echo "for (i = $first; i <= $last; i+=$incr) i" | bc -l
)

An advantage is that you're not limited by the size/resolution of your CPU's doubles:

$ seq '(2^200)' '(2^100)' '(2^200+2^102)'
1606938044258990275541962092341162602522202993782792835301376
1606938044258990275541962092342430253122431223184289538506752
1606938044258990275541962092343697903722659452585786241712128
1606938044258990275541962092344965554322887681987282944917504
1606938044258990275541962092346233204923115911388779648122880

But beware of line wrapping when the numbers get too big:

$ seq '(2^500)' '(2^100)' '(2^500+2^101)'
32733906078961418700131896968275991522166420460430647894832913680961\
33796404674554883270092325904157150886684127560071009217256545885393\
053328527589376
32733906078961418700131896968275991522166420460430647894832913680961\
33796404674554883270092325904157150886684127560071010484907146113622\
454825230794752
32733906078961418700131896968275991522166420460430647894832913680961\
33796404674554883270092325904157150886684127560071011752557746341851\
856321934000128
7

According to the open group POSIX awk supports BEGIN, therefore it can be done in awk:

awk -v MYEND=6 'BEGIN { for(i=1;i<=MYEND;i++) print i }'

Where -v MYEND=6 would stand for the assignment as in the first argument to seq. In other words, this works too:

seq() {
    end=$1
    awk -v end=$end 'BEGIN { for( i = 1; i <= end; i++) print i }'
}

Or even with the three variables start, increment and end. (This doesn't support a negative increment nor floats):

seq() { 
    if [ "$#" = 1 ]; then
        start=1 incr=1 end=$1
    elif [ "$#" = 2 ]; then
        start=$1 incr=1 end=$2
    elif [ "$#" = 3 ]; then
        start=$1 incr=$2 end=$3
    else 
        echo "error: invalid number of arguments" >&2
        return 1
    fi
    if ! [ "$incr" -ge 1 ]; then
        echo "error: invalid increment (must be >= 1)" >&2
        return 1
    fi
    awk -v start=$start -v incr=$incr -v end=$end '
        BEGIN {  for( i = start; i <= end; i += incr) print i }'
}

Extra Solaris note: On Solaris /usr/bin/awk is not POSIX compliant, you need to use either nawk or /usr/xpg4/bin/awk on Solaris.

On Solaris, you probably want to set /usr/xpg4/bin early in PATH if you are running a POSIX compliant script.

Reference answer:

8
  • 1
    I didn't even think to use awk for this. Thanks!
    – Elizafox
    Jul 25, 2016 at 17:39
  • I also have a perl version of the script, but awk is an excellent suggestion.
    – Jeff Schaller
    Jul 25, 2016 at 17:42
  • 1
    On Solaris, you need to use /usr/bin/nawk not /usr/bin/awk.
    – fd0
    Jul 25, 2016 at 18:31
  • 1
    this could easily be extended to support arbitrary start and increment parameters as well (ala Linux seq)
    – Jeff Schaller
    Jul 25, 2016 at 19:23
  • 2
    Solaris old-awk does have BEGIN (and END too); what it lacks is 'BEGIN plus no middle auto-exit'. You can fix this by saying exit in your BEGIN action, or by supplying input that reaches EOF either by redirection or a file operand; /dev/null is good because it is always present and always empty. Although in general nawk or xpg4 is more convenient. Jul 26, 2016 at 4:23
5

while is POSIX

i=1
while [ "$i" -ne 11 ]
do    
    echo $i
    i=$(( $i + 1 ))
done

while is portable, versatile, and frequently under used.

3

I ended up writing one in ksh a while ago; it's not as bullet-proof as it could be, because I wrote it for myself, but it should be a good start. This should work with a pure POSIX shell also.

#!/bin/sh

start=1
end=1
step=1

case $# in
        0)
                echo Usage: $0 '[Start [Step]] End'
                exit 0
                ;;
        1)
                end=$1
                ;;
        2)
               start=$1
               end=$2
               ;;
        3)
               start=$1
               step=$2
               end=$3
               ;;
esac

# quick & dirty validations
if [ $step -eq 0 ]
then
  exit 1
fi

if [ $step -gt 0 ]
then
  if [ $start -gt $end ]
  then
    exit 2
  fi
else
  if [ $start -lt $end ]
  then
    exit 3
  fi
fi

i=$start
if [ $step -gt 0 ]
then
  while [ $i -le $end ]
  do
    printf "%d\n" $i
    i=$(( i + step ))
  done
else
  while [ $i -ge $end ]
  do
    printf "%d\n" $i
    i=$(( i + step ))
  done
fi
0
0

Here is the shortest (primary goal) and cheapest (in terms of spent system resources - secondary goal) variant I could come up with:

while :; do echo x; done | sed 10q

Replace the "10" with the desired number of iterations.

One can use it in a "for"-loop as follows:

for i in `while :; do echo x; done | sed 10q`; do echo hello; done

It can be used more efficiently in a "while" loop however:

while :; do echo x; done | sed 10q | while read x; do echo hello; done

This avoids one process invocation and temporary storage of a potentially large number of "x" characters as "for" arguments.

The most efficient way I could come up was

i=10; while case $i in 0) break; esac; do i=$(($i - 1)); echo hello; done

which only uses shell-builtins except for the "echo" and needs no other process invocations. Unfortunately, it is quite ugly and unpleasant to see in a script. However, if script performance is paramount, it is the best portable solution I could think of.

Of course, awk and bc can do the job also. But both are much more powerful utilities than required for this job, and therefore using them for such a simple task seems to be a waste of system resources to me.

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .