lisp/combos.lisp @ a65fd2691c94 default tip

More
author Steve Losh <steve@stevelosh.com>
date Mon, 03 Nov 2025 14:55:17 -0500
parents (none)
children (none)
(eval-when (:compile-toplevel :load-toplevel :execute)
  (ql:quickload '(:adopt :str :alexandria) :silent t))

(defpackage :combos
  (:use :cl)
  (:export :toplevel :*ui*))

(in-package :combos)


;;;; Configuration ------------------------------------------------------------
(defparameter *n* 2)
(defparameter *trim* t)
(defparameter *mode* nil)
(defparameter *output-sep-field* " ")
(defparameter *output-sep-record* (string #\newline))
(defparameter *input-sep* (string #\newline))

(defvar *first* t)

;;;; Functionality ------------------------------------------------------------
(defun print-record (fields)
  (if *first*
    (setf *first* nil)
    (write-string *output-sep-record*))
  (write-string (str:join *output-sep-field* fields)))

(defun print-record% (&rest fields)
  (print-record fields))

(defun run/permutations (lines) (alexandria:map-permutations #'print-record lines :length *n*))
(defun run/combinations (lines) (alexandria:map-combinations #'print-record lines :length *n*))
(defun run/derangements (lines) (alexandria:map-derangements #'print-record lines))
(defun run/product (lines)
  (apply #'alexandria:map-product #'print-record% (loop :repeat *n* :collect lines)))

(defun run ()
  (let* ((input (alexandria:read-stream-content-into-string *standard-input*))
         (lines (str:split *input-sep* input :omit-nulls t)))
    (when *trim*
      (setf lines (mapcar #'str:trim lines)))
    (setf *first* t)
    (funcall (ecase *mode*
               (:permutations #'run/permutations)
               (:combinations #'run/combinations)
               (:derangements #'run/derangements)
               (:product #'run/product))
             lines)))


;;;; User Interface -----------------------------------------------------------
(defparameter *option/help*
  (adopt:make-option 'help
    :help "Display help and exit."
    :long "help"
    :short #\h
    :reduce (constantly t)))

(defparameter *option/version*
  (adopt:make-option 'version
    :help "Display version information and exit."
    :long "version"
    :reduce (constantly t)))

(defparameter *option/n*
  (adopt:make-option 'n
    :help "Size of permutations/combinations/derangements (default 2)."
    :parameter "N"
    :long "length"
    :short #\n
    :reduce #'adopt:last
    :initial-value 2
    :key #'parse-integer))

(defparameter *option/mode/permutations*
  (adopt:make-option 'mode
    :help "Output permutations of the input lines."
    :long "permutations"
    :short #\P
    :reduce (constantly :permutations)))

(defparameter *option/mode/combinations*
  (adopt:make-option 'mode
    :help "Output combinations of the input lines."
    :long "combinations"
    :short #\C
    :reduce (constantly :combinations)))

(defparameter *option/mode/derangements*
  (adopt:make-option 'mode
    :help "Output derangements of the input lines (ignores -n)."
    :long "derangements"
    :short #\D
    :reduce (constantly :derangements)))

(defparameter *option/mode/product*
  (adopt:make-option 'mode
    :help "Output cartesian product of the input lines."
    :long "cartesian-product"
    :short #\X
    :reduce (constantly :product)))

(defparameter *option/input-sep*
  (adopt:make-option 'input-sep
    :help "Input separator (default newline)."
    :parameter "SEP"
    :long "input-separator"
    :short #\i
    :reduce #'adopt:last
    :initial-value (string #\newline)))

(defparameter *option/input-sep-nul*
  (adopt:make-option 'input-sep-nul
    :result-key 'input-sep
    :help "Use NUL byte as input separator."
    :long "input-separator-0"
    :reduce (constantly (string #\nul))))

(defparameter *option/output-sep/field*
  (adopt:make-option 'output-sep/field
    :help "Output field separator (default \" \")."
    :parameter "SEP"
    :long "output-field-separator"
    :short #\f
    :reduce #'adopt:last
    :initial-value " "))

(defparameter *option/output-sep/field-nul*
  (adopt:make-option 'output-sep/field-nul
    :result-key 'output-sep/field
    :help "Use NUL byte as field separator."
    :long "output-field-separator-0"
    :reduce (constantly (string #\nul))))

(defparameter *option/output-sep/record*
  (adopt:make-option 'output-sep/record
    :help "Output record separator (default newline)."
    :parameter "SEP"
    :long "output-record-separator"
    :short #\r
    :reduce #'adopt:last
    :initial-value (string #\newline)))

(defparameter *option/output-sep/record-nul*
  (adopt:make-option 'output-sep/record-nul
    :result-key 'output-sep/record
    :help "Use NUL byte as record separator."
    :long "output-record-separator-0"
    :reduce (constantly (string #\nul))))

(adopt:defparameters (*option/trim* *option/no-trim*)
  (adopt:make-boolean-options 'trim
    :short #\t
    :long "trim"
    :help "Trim whitespace from input (the default)."
    :help-no "Do not trim whitespace from input."
    :initial-value t))


(adopt:define-string *help-text*
  "combos takes entries from standard input, combines them (as permutations, ~
   combinations, derangements, or the cartesian product) and outputs the ~
   resulting combos to standard output.")

(defparameter *examples*
  '(("Print 2-length permutations of 1..4:"
     . "seq 4 | combos --permutations -n 2")
    ("Print comma-separated 3-length combinations of 1..5:"
     . "seq 5 | combos --combinations -n 3 -f ,")
    ("Print every possible pairing of NUL delimited input:"
     . "find . -name '*.lisp' -print0 | combos --cartesian-product --input-separator-0 -n 2")))

(defparameter *ui*
  (adopt:make-interface
    :name "combos"
    :usage "MODE [OPTIONS]"
    :summary "combine and output lines of input"
    :help *help-text*
    :examples *examples*
    :contents (list
                *option/help*
                *option/version*
                (adopt:make-group 'input-options
                  :title "Input Options"
                  :options (list *option/input-sep*
                                 *option/input-sep-nul*
                                 *option/trim*
                                 *option/no-trim*))
                (adopt:make-group 'output-options
                  :title "Output Options"
                  :options (list *option/mode/permutations*
                                 *option/mode/combinations*
                                 *option/mode/derangements*
                                 *option/mode/product*
                                 *option/n*
                                 *option/output-sep/field*
                                 *option/output-sep/field-nul*
                                 *option/output-sep/record*
                                 *option/output-sep/record-nul*)))))


(defun toplevel ()
  (adopt::quit-on-ctrl-c ()
    (multiple-value-bind (arguments options) (adopt:parse-options-or-exit *ui*)
      (when (gethash 'help options)
        (adopt:print-help-and-exit *ui*))
      (let ((*mode* (gethash 'mode options))
            (*n* (gethash 'n options))
            (*output-sep-field* (gethash 'output-sep/field options))
            (*output-sep-record* (gethash 'output-sep/record options))
            (*input-sep* (gethash 'input-sep options))
            (*trim* (gethash 'trim options)))
        (handler-case
            (progn
              (assert (null arguments) () "No arguments accepted.")
              (run))
          (error (e)
                 (adopt:print-error-and-exit e)))))))