hmisc icon indicating copy to clipboard operation
hmisc copied to clipboard

Collection of miscellaneous helper algorithms and types. Helper functions for strings, exceptions etc.

#+title: readme

I don't want to call this library officially "deprecated", but I haven't been paying as much attention to the code quality and/or in recent times. I think everything up until version ~0.14.4~ can be considered "mostly reliable" - so if you are looking to use this library, I would advise you to pin dependency to this version.

Maybe in the future I will return to working on this library - for now I don't think I have the energy required to constantly fight nim documentation generator, write unit testing framework and test runner and so on.


Collection of miscellaneous helper algorithms and types. Over time this library accumulated a lot of functionality, not all of this is necessary and almost everything was made on case-by-case basis instead of being designed at once.

Most useful parts of the library are probably

  1. ~hmisc/other/oswrap~ - wrapper for the ~std/os~ but with support for proper ~disctinct~ types
  2. ~hmisc/other/hshell~ - shell command execution builder
  3. ~hmisc/core/gold~ or ~hmisc/core/all~ - these contain my import-everywhere templates, procedures and macros.

In addition to the general helpers, the library also contains forks of some of the fusion modules - specifically ~hmisc/types/hmap~ is a fork of ~fusion/btreetables~ with saner type naming (no more type clashes all over the place) and couple helper routines. I also copied ~fusion/matching~ back, since fusion can be considered effective dead and deprecated - not tagged (at the time of writing ~requires fusion~ still pulls 9-month old version, that contains none of the fixes that was made to pattern matching after that. For more details see [[https://forum.nim-lang.org/t/8627#56155][forum thread]] - not especially interesting, but if you want more context ...). Right now it is a simple copy, in the future I will clean up the implementation since I've got several ideas on how it can be improved. DSL will be cleaned up as well, right now it is really overloaded.

** Installation:

#+begin_src sh nimble install hmisc #+end_src

** Links

  • [[https://nimble.directory/pkg/hmisc][nimble package]]
  • [[https://haxscramper.github.io/hmisc/theindex.html][documentation]]
  • [[https://github.com/haxscramper/hmisc][github]]
  • Macros

** ~hmisc/macros/iflet~

Rust-like iflet. Get value from ~Option[T]~ only if it contains something, otherwise execute ~else~.

#+begin_src nim :exports both import options import hmisc/macros/iflet

iflet (val = some(12)): echo typeof val

iflet (val = none(int)): echo "???" else: echo "no value" #+end_src

#+RESULTS: : int : no value

  • Algorithms :PROPERTIES: :header-args:nim:+ :import hmisc/algo/halgorithm :END:

** ~hmisc/algo/halgorithm~ [[https://haxscramper.github.io/hmisc/src/hmisc/algo/halgorithm.html][documentation]]

  • predicates
    • ~allOfIt~
    • ~anyOfIt~
    • ~noneOfIt~
    • ~==~ for Option-Val comparison
  • working with sequences
    • ~maxIt~
    • ~disjointIter~
    • ~last~
  • working with strings
    • ~joinw~, ~joinq~, ~joinl~, ~joinkv~ - join on whitespaces, whitespace + quote each string, newlines or key-value pairs. Mostly useful in ~strformat.&~ - can write ~{somevar.join()}~ instead of ~{somevar.join("\n")}~
    • ~wrap~ - wrap strings in delimiters. Has convinience overload for ~.wrap("()")~ that automatically determines starting/ending wrapper strings.
    • Multiple overloads for ~join~ and ~startsWith~
    • ~enclosedIn~ - check if string is wrapped in delimiters
    • ~dedent~ - decease indentation for multiline string
    • ~camelSplit~ - split string as camel case identifier
    • ~abbrevCamel~ - /camelCase/ abbreviation search.
    • Several variations of ~dropPrefix~, ~addPrefix~, ~startsWith~, ~addSuffix~ for less common use cases
    • Filtering sequence of strings by prefix
    • Dropping subsequence in strings
    • Finding common prefix in sequence of strings
  • other
    • ~ifSomeIt~ - same as ~opt.isSome() and (let it = opt.get(); predicate)~
    • ~testEq~ - compare two objects. If they are different print first mismatching line in their string representation.
    • ~assertEq~ - compare objects using ~testEq~, raise on failed comparison.

#+begin_src nim :exports both import hmisc/algo/halgorithm, std/strformat let v = @["w234", "333"]

echo ": ", &"{v.joinq()}"

block: echo "-- withIt --" let immutable = (a: 12, b: 12) echo immutable.withIt do: it.a = 909

block: echo "-- withResIt --" let immutable = (a: 12, b: "eee") echo immutable.withResIt do: it.a += 999 $it.a & it.b

block: echo "-- join* --" echo {1 : "22", 3: "333"}.joinkv().join()

block: echo "-- abbrevCamel --" echo abbrevCamel("AA", @["ABA", "AZZ", "A)"]) #+end_src

#+RESULTS: : : "w234" "333" : -- withIt -- : (a: 909, b: 12) : -- withResIt -- : 1011eee : -- join* -- : 0 = (1, "22") 1 = (3, "333") : -- abbrevCamel -- : @["ABA"]

** ~hmisc/algo/hseqdistance~ [[https://haxscramper.github.io/hmisc/src/hmisc/algo/hseqdistance.html][documentation]]

Fuzzy string matching and generic longest common subsequece implementation

  • ~longestCommonSubsequence~ - generic implementation of LCS algorithm for ~seq[T]~
  • ~fuzzyMatch~ - weighted sequence fuzzy match. Compare each element in the sequence to pattern and assign similarity score. Should behave similarly to ~fzf~ or sublime text. Reimplementation of [[https://www.forrestthewoods.com/blog/reverse_engineering_sublime_texts_fuzzy_match/]['Reverse engineering subtime text's fuzzy match']]. I haven't used it in any interactive applications as of yet, but there are some unit tests. It has generic implementation and somewhat annoying to use, but provides very flexible interface, allowing to completely customize how fuzzy matching is performed.

#+begin_src nim :exports both import hmisc/doc_examples

echo "# ~~~~ leading / ~~~~ #\n|" matchTest "//hell.txt", "/nice/we/hell.txt": if other[matches[0]] == '/': 1000 # high cost if have exact match with starting / else: matches.sum()

echo "|\n# ~~~~ no leading / ~~~~ #\n|" matchTest "nicehell.txt", "/nice/we/hell.txt": if other[matches[0]] == '/': 1000 else: matches.sum() #+end_src

#+RESULTS: : # ~~~~ leading / ~~~~ # : | : input: /nice/we/hell.txt //hell.txt :1000 : match: / / hell.txt : | : # ~~~~ no leading / ~~~~ # : | : input: /nice/we/hell.txt nicehell.txt :113 : match: nic e hell.txt

** ~hmisc/algo/hseq_mapping~

  • ~deduplicateIt~
  • ~mapPairs~ :: ~mapIt~ for types that implement ~pairs~ iterator, or ~items~ that return tuple, or sequence of tuples. Inject index of the item, ~lhs~ (first element) and ~rhs~ (second element). Should correctly handle ~{.requiresinit.}~ fields.

** ~hmisc/algo/htree_mapping~

  • ~mapItBFStoSeq~ :: iterate over tree in BFS order, store mapping result in sequence.
  • ~iterateItBFS~ :: iterate over tree in BFS order
  • ~iterateItDFS~ :: iterate over tree in DFS order. Uses iterative DFS instead of recursive call.
  • ~mapItDFS~ :: ~mapIt~ for converting trees in DFS order
  • Types

** ~hmisc/types/colorstring~

Easier manipulation of colored strings in terminal. Support splitting regular strin in same-color chunks, finding 'visible' length of the string (as printed in terminal). Helper functions like ~toYellow()~ or ~toRed()~ to make creation of the colored strings simpler. All attributes from ~terminal~ module are supported (fg/bg colors and modifiers).

Provides two types for colored text - ~ColoredString~ (string + styling) and ~ColoredRune~ (unicode rune + styling).

  • Other

~hshell~ and ~oswrap~ modules provide more strictly typed wrappers for tasks that are usually performed using simple string concatenations. You get better static safety guarantees (not possible to pass relative path to function expecting absolute one) and less headaches related to correct quoting/CLI command syntax at the expense of little more verbose code.

~oswrap~ is a ~1:1~ mapping of ~std/os~ and is expected to have all functions reimplemented (wrapped).

~hshell~ also treats non-zero return codes as exceptions, so you can just execute shell commands without endless checks for ~code != 0: echo "oh no!"~. This can be turned off, but works by default, so when writing ~let (output, err, _) = runShell("someCommand")~ you will be sure that failures won't be silently ignored.

** ~hmisc/other/oswrap~

Wrapper on top of ~os~ and ~nimscsrip~ that allows to use the same code on ~c~ and ~nimscript~ targets. Some helper templates/functions are introduced. Provide distinct string for files/directories - e.g. ~RelDIr = distinct string~ as well as overloads for almost all functions in ~os~ module.

NOTE it is expected to be imported instead of ~os~ module - functions without arguments were update to use ~distinct~ types too, so if two modules are imported togetether frequent type clashes are expected.

  • ~mkDir~, ~getEnv~, ~delEnv~, ~toExe~, ~listDirs~, ~rmFile~, ~mkDir~, ~mvFile~, ~cpFile~, ~cpDir~, ~cd~, ~cwd~ - default file/directory manipulation functions
  • ~ (prefix tilda) prefix operator to get path relative to home directory. Same as ~getHomeDir() / path~
  • ~&&~ join shell command strings with correct spacing
  • ~withDir~ - temporarily set directory for body
  • ~withEnv~ - temporarily set environment variables for body

** ~hmisc/other/hshell~ [[https://haxscramper.github.io/hmisc/src/hmisc/other/hshell.html][documentation]]

Helper functions for running shell commands - reduce need for string concatenation for shell - ~Cmd~ object supports adding commands/flags/options/subcommands/arguments while deferring conversion to string as long as possible and taking care of correct syntax (correct dashes for ~X11~ CLI tools (always single prefix dash), key-value separators (nim tooling uses ~:~, GNU is most likely to expect ~=~ or spaces)).

Possible use case: Imagine you need to write a script that launches new docker container, mounts some folders, copy files over, and perform some nontrivial commands inside container (and command is not predetermined - it is also has to be built in advance).

Regular approach would be to cobble together one giant string that will then be executed via ~startProcess~. You need to then check for return code, and hope that you haven't messed up quoting, argument syntax for particular command and so on (nim tooling uses ~:~, GNU - ~=~ and so on).

~hshell~ hopefully provides solution to most of the usability of command line programs - you no longer need to worry about correct spacing, quoting and other stuff like that. Instead, you just build AST for command to be executed, using set of convenient operators and functions.

#+begin_src nim let cmd = shCmd("nimble", "install")

Nice side effect - you can now comment on different flags and use

checks/loops without worrying about correct

spacing/concatnation/prefixes etc.

let doCleanup = true let dockerCmd = shCmd("docker").withIt do: it.cmd "run" # Add subcommand it - "i" it - "t" if doCleanup: it - "rm" # Remove container after test execution it - ("v", "/tmp/tmp-mount:/project") # Key-value pair it.arg "nim-base" it.arg "sh" it - "c" it.expr: shAnd: shCmd(cd, "/project/main") cmd # Can easily build complicated commands from variables #+end_src

NOTE: no special DSL syntax is introduced, just couple of overlads for common use cases (~-~ proc for flags/options)

  • ~runShell~ Raise exception when command has exited with non-zero code (because you will be checking return code anyway), get stderr and stdout separately. Uses fallback ~exec~ and ~gorgeEx~ on nimscript targets and tries to emulate compiled behaviour as close as possible (respect execution flags).
  • ~iterstdout~ - iterate each line for executed program's stdout
  • ~execShell~ - execute shell command, redirect output into parent streams.

NOTE: You might consider this module a 'shell program wrapper'. It was created to make using external processes from your code easier and safer. No need to check return codes all the time, think about quoting, correct arguments and so on. Thus said - it is quite difficult to wrap all complixity of the command line interfaces, even with quite sophisticated logic. Several escape hatches are present, to still pass almost arbitrary strings for shell execution. First: ~ShellExpr~ - thin wrapper, ~distinct string~. Second is ~raw()~ function for setting command line arguments.

  • Contribution & development

Most of the features in this library were implemented on /do-it-when-I-need-it/ basis. Some of them are tested quite extensively (sequence and tree mappings, colored strings), but more unit test are always welcome.

  • References
  • [[https://research.google/pubs/pub44667/][A new apprach to optimal code formatting]]