devinteske

excombine

Jan 12th, 2018
642
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
  1. #!/bin/sh
  2. #-
  3. ############################################################ COPYRIGHT
  4. #
  5. # (c)2002-2014. Devin Teske. All rights reserved.
  6. #
  7. # Redistribution and use in source and binary forms, with or without
  8. # modification, are permitted provided that the following conditions
  9. # are met:
  10. # 1. Redistributions of source code must retain the above copyright
  11. #    notice, this list of conditions and the following disclaimer.
  12. # 2. Redistributions in binary form must reproduce the above copyright
  13. #    notice, this list of conditions and the following disclaimer in the
  14. #    documentation and/or other materials provided with the distribution.
  15. #
  16. # THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
  17. # ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING BUT NOT LIMITED TO, THE
  18. # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
  19. # ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
  20. # FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
  21. # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
  22. # OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
  23. # HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
  24. # LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
  25. # OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  26. # SUCH DAMAGE.
  27. #
  28. ############################################################ INFORMATION
  29. #
  30. # Usage: excombine [-g] [-r range] [-c command] item1 item2 ...
  31. #
  32. # See `FUNCTIONS' below for a full-and-complete description of the above
  33. # parameters.
  34. #
  35. # Simple Example:
  36. #   # Print all combinations of 2 and 3 letters
  37. #   excombine -r 2..3 a b 1 2
  38. #   a b
  39. #   a 1
  40. #   a 2
  41. #   b 1
  42. #   b 2
  43. #   1 2
  44. #   a b 1
  45. #   a b 2
  46. #   a 1 2
  47. #   b 1 2
  48. #
  49. # Advanced Example:
  50. #   # Print all combinations where numbers add up to 13
  51. #   excombine -c 'sum=$( echo $items | tr " " "+" ); echo $sum=$(
  52. #       echo $sum | bc )' 1 2 3 4 5 6 7 8 | grep '=13$'
  53. #   5+8=13
  54. #   6+7=13
  55. #   1+4+8=13
  56. #   1+5+7=13
  57. #   2+3+8=13
  58. #   2+4+7=13
  59. #   2+5+6=13
  60. #   3+4+6=13
  61. #   1+2+3+7=13
  62. #   1+2+4+6=13
  63. #   1+3+4+5=13
  64. #
  65. ############################################################ GLOBALS
  66.  
  67. # Global exit status variables
  68. SUCCESS=0
  69. FAILURE=1
  70.  
  71. ############################################################ FUNCTIONS
  72.  
  73. # f_getword $word $var_to_set $word1 $word2 ...
  74. #
  75. # Set variable $var_to_set to the expansion of $word against a series of
  76. # positional arguments.
  77. #
  78. # Example 1:
  79. #
  80. #   f_getword 2 word dog bird cat # word=[bird]
  81. #
  82. # Example 2:
  83. #
  84. #   list="word1   word2   word3"
  85. #   f_getword 3 word $list # word=[word3]
  86. #
  87. f_getword()
  88. {
  89.     local __word="$1" __var_to_set="$2"
  90.     shift 2 # word var_to_set
  91.     eval $__var_to_set=\"\${$__word}\"
  92. }
  93.  
  94. # excombine [-g] [-r $range] [-c $command] $item1 $item2 ...
  95. #
  96. # Perform an action (`-c $command') for every combination in a list of items
  97. # (default is to print the combination to the screen) with a given set
  98. # (`-r num') or range of sets (`-r first..last' or `-r first-last'). See below
  99. # for a description of each parameter.
  100. #
  101. # Options:
  102. #
  103. # -g          Don't stop if $command (default echo) returns error, but keep
  104. #             going after an error is returned.
  105. #
  106. # -r $range   A number dictating how many items appear in the combinations.
  107. #             For example: if the number `5' is used, only combinations that
  108. #             use a total of 5 items will be calculated. If you pass the
  109. #             number `0', all ranges will be calculated (this is the default).
  110. #             You may also specify a range of ranges to be calculated. This can
  111. #             be done by separating two numbers with a series of two periods or
  112. #             a hyphen [-]. For example: passing `1..2' will (if possible)
  113. #             calculate all combinations where both the total of items in the
  114. #             combination are one (1) and also where the total of items in the
  115. #             combination is two. If the range exceeds the possible total of
  116. #             items, no error occurs; the function simply does as many sets as
  117. #             possible. If the range is negative, it will not cause an error;
  118. #             since this function is based on mathematics, negative values are
  119. #             accepted but in theory and practice, have no meaning. Avoid using
  120. #             negative values as they will have unpredictable results.
  121. #
  122. # -c $command The command to be executed for each iteration (e.g., `echo').
  123. #             Note however that your command must make use of `$items' to
  124. #             access the calculated combination for each iteration. Also note
  125. #             that proper escaping is required to get the dollar-symbol [$] to
  126. #             be passed in without the shell attempting to expand it (either
  127. #             place the symbol within single-qotes or precede it with a back-
  128. #             slash [\]).
  129. #
  130. excombine()
  131. {
  132.     local range=0 command='echo $items' keep_going=
  133.     local startset endset
  134.  
  135.     #
  136.     # Process optional arguments
  137.     #
  138.     local OPTIND flag
  139.     while getopts "gr:c:" flag > /dev/null; do
  140.         case "$flag" in
  141.         g) keep_going=1 ;;
  142.         r) range="$OPTARG" ;;
  143.         c) command="$OPTARG" ;;
  144.         esac
  145.     done
  146.     shift $(( $OPTIND - 1 ))
  147.  
  148.     #
  149.     # Process a possible range of `sets'
  150.     #
  151.     case "$range" in
  152.     *..*) startset="${range%%..*}" endset="${range##*..}";;
  153.      *-*) startset="${range%%-*}" endset="${range##*-}";;
  154.        *) startset="$range" endset="$range"
  155.     esac
  156.  
  157.     #
  158.     # Process special-case range `0' (default) to produce all range sets
  159.     #
  160.     local nitems=$#
  161.     [ ${startset:-0} -eq 0 ] && startset=1
  162.     [ ${endset:-0} -eq 0 ] && endset=$nitems
  163.  
  164.     #
  165.     # Flip negative range values positive (pedantic)
  166.     # NB: Negative values will throw off the math
  167.     #
  168.     [ $startset -lt 0 ] && startset=$(( $startset * (0-1) ))
  169.     [ $endset -lt 0 ] && endset=$(( $endset * (0-1) ))
  170.  
  171.     #
  172.     # Adjust values to be non-zero (mathematical constraint)
  173.     #
  174.     [ $startset -eq 0 ] && startset=1
  175.     [ $endset -eq 0 ] && endset=1
  176.  
  177.     #
  178.     # Enforce limits so we don't run over bounds
  179.     #
  180.     [ $startset -le $nitems ] || startset=$nitems
  181.     [ $endset -le $nitems ] || endset=$nitems
  182.  
  183.     #
  184.     # Set the direction of program flow (incrementing vs. decrementing)
  185.     # NB: For the below loop through the data-set range
  186.     #
  187.     local next=$(( (0-1) ))
  188.     [ $startset -le $endset ] && next=1
  189.  
  190.     #
  191.     # Pre-register locals before entering a loop to prevent unbridled
  192.     # memory growth (local does not check before making a variable local).
  193.     #
  194.     local num_subsets ncombos items combo
  195.     local z d n # math transients
  196.     local argnum argnums argnums_backend argnums_last argnums_new
  197.     local setpos setpos_backend
  198.     local seed
  199.  
  200.     #
  201.     # Loop over each `set' in the configured direction until we hit the end
  202.     # NB: Each `set' can represent either multiple items or a single item
  203.     #
  204.     local curset=$startset retval
  205.     while [ $curset -le $endset ]; do
  206.  
  207.         #
  208.         # Calculate number of subsets, based on the number of items in
  209.         # the current set we're working on
  210.         #
  211.         num_subsets=$(( $nitems - $curset + 1 ))
  212.  
  213.         #
  214.         # Calculate number of combinations based on number of subsets
  215.         # NB: Using a loop here because sh(1) has no exponent operator
  216.         # NB: sh(1), unlike bash(1), doesn't support `**' for exponent
  217.         #
  218.         z=1 d=1 n=0
  219.         while [ $n -lt $curset ]; do
  220.             z=$(( $z * ($num_subsets + $n) ))
  221.             d=$(( $d * ($n + 1) ))
  222.             n=$(( $n + 1 ))
  223.         done
  224.         ncombos=$(( $z/$d ))
  225.  
  226.         #
  227.         # Fill $items with the initial positional arguments
  228.         #
  229.         # E.g., if this is a set of 2 and positional arguments are `a'
  230.         # and `b', we'll set $items to `a b'.
  231.         #
  232.         n=1 items=
  233.         while [ $n -le $curset ]; do
  234.             eval 'items="$items ${'$n'}"'
  235.             n=$(( $n + 1 ))
  236.         done
  237.         items="${items# }"
  238.  
  239.         #
  240.         # Produce results with this first set of $items
  241.         #
  242.         eval $command
  243.         retval=$?
  244.         [ $retval -eq $SUCCESS -o "$keep_going" ] || return $FAILURE
  245.  
  246.         #
  247.         # Prefill two `arrays' (really IFS separated strings); both
  248.         # arrays containing a linear sequence of integers representing
  249.         # the argv positions that we will need to reference in the
  250.         # following subset loop.
  251.         #
  252.         # The first sequence ($argnums) is a linear sequence starting
  253.         # at one (1) and ending at N (where N is the same integer as
  254.         # the current set we're operating on). E.g., if we're currently
  255.         # operating on a set-of-2, $argnums is set to "1 2".
  256.         #
  257.         # The second sequence ($argnums_backend) is a linear sequence
  258.         # starting at $nitems-N and ending at $nitems (again, N is the
  259.         # same integer as the current set we're operating on; $nitems
  260.         # is the total number of items in $list). E.g., if we're
  261.         # operating on a set-of-2, and $nitems is 8, $argnums_backend
  262.         # is set to "7 8".
  263.         #
  264.         # These two arrays are used for matrix calculations below in
  265.         # the subset loop.
  266.         #
  267.         n=1 argnums=
  268.         while [ $n -le $curset ]; do
  269.             argnums="$argnums $n"
  270.             n=$(( $n + 1 ))
  271.         done
  272.         n=$curset argnums_backend=
  273.         while [ $n -gt 0 ]; do
  274.             argnums_backend="$argnums_backend $((
  275.                 $nitems - $n + 1
  276.             ))"
  277.             n=$(( $n - 1 ))
  278.         done
  279.  
  280.         #
  281.         # Process remaining self-similar combinations in the set
  282.         #
  283.         combo=1
  284.         while [ $combo -lt $ncombos ]; do
  285.  
  286.             #
  287.             # Using self-similarity (matrix) theorem, determine
  288.             # (by comparing the [sliding] $argnums to the stored
  289.             # $argnums_backend) the number of arguments that remain
  290.             # available for shifting into a new $argnums value (for
  291.             # later mapping into $items).
  292.             #
  293.             # In essence, determine when $argnums has slid into
  294.             # $argnums_backend in which case we can mathematically
  295.             # use the last item to find the next-new item.
  296.             #
  297.             n=$curset
  298.             while [ $n -gt 0 ]; do
  299.                 f_getword $n setpos $argnums
  300.                 f_getword $n setpos_backend $argnums_backend
  301.                 #
  302.                 # If $setpos is equal to or greater than
  303.                 # $setpos_backend then we keep iterating over
  304.                 # the current set's list of argument positions
  305.                 # until otherwise; each time incrementing the
  306.                 # amount of numbers we must produce from
  307.                 # formulae rather than stored position.
  308.                 #
  309.                 argnums_last=$n
  310.                 [ $setpos -lt $setpos_backend ] && break
  311.                 n=$(( $n - 1 ))
  312.             done
  313.  
  314.             #
  315.             # The next few stanzas are dedicated to rebuilding
  316.             # the $argnums pseudo-array for mapping positional
  317.             # arguments [immediately after] into $items.
  318.             #
  319.  
  320.             #
  321.             # Fill an $argnums_new array (again, really just an IFS
  322.             # separated string) with all but new/latest item(s).
  323.             #
  324.             n=1 argnums_new=
  325.             while [ $n -lt $argnums_last ]; do
  326.                 f_getword $n argnum $argnums
  327.                 argnums_new="$argnums_new $argnum"
  328.                 n=$(( $n + 1 ))
  329.             done
  330.  
  331.             #
  332.             # Get the generator number used to populate unknown
  333.             # positions in the matrix (using self-similarity).
  334.             #
  335.             f_getword $argnums_last seed $argnums
  336.  
  337.             #
  338.             # Use the generator number to populate any position
  339.             # numbers that weren't carried over from previous
  340.             # combination run -- using self-similarity theorem.
  341.             #
  342.             n=$argnums_last
  343.             while [ $n -le $curset ]; do
  344.                 argnums_new="$argnums_new $((
  345.                     $seed + $n - $argnums_last + 1
  346.                 ))"
  347.                 n=$(( $n + 1 ))
  348.             done
  349.  
  350.             #
  351.             # Finally, assign newly calculated $argnums
  352.             #
  353.             argnums="$argnums_new"
  354.  
  355.             #
  356.             # Now map the new $argnums into values stored in $items
  357.             #
  358.             n=1 items=
  359.             while [ $n -le $curset ]; do
  360.                 f_getword $n argnum $argnums
  361.                 eval 'items="$items ${'$argnum'}"'
  362.                 n=$(( $n + 1 ))
  363.             done
  364.             items="${items# }"
  365.  
  366.             #
  367.             # Produce results with this set of $items
  368.             #
  369.             eval $command
  370.             retval=$?
  371.             [ $retval -eq $SUCCESS -o "$keep_going" ] ||
  372.                 return $FAILURE
  373.  
  374.             combo=$(( $combo + 1 ))
  375.         done
  376.  
  377.         curset=$(( $curset + $next ))
  378.     done
  379. }
  380.  
  381. # usage
  382. #
  383. # Print a simple usage statement and exit.
  384. #
  385. usage()
  386. {
  387.     echo "Usage: $0 [-g] [-r range] [-c command] item1 item2 ..."
  388.     exit $FAILURE
  389. }
  390.  
  391. ############################################################ MAIN
  392.  
  393. # Print usage and exit if no arguments were passed
  394. [ $# -gt 0 ] || usage
  395.  
  396. excombine "$@"
  397.  
  398. ################################################################################
  399. # END
  400. ################################################################################
RAW Paste Data