Zeptoforth on the PicoCalc

Globs!

I created a glob tool for zeptoforth, This should save much typing where previously one had to individually type each desired path into one’s PicoCalc one by one.

The source code is at extra/common/glob.fs. Note that this is not in a release yet.

This tool is a loadable extra but is now installed by the PicoCalc installer scripts unless one selects the rp2040 platform, for lack of flash dictionary space.

It is used by passing a glob pattern along with an execution token into fat32-tools::glob ( pattern-addr pattern-bytes xt -- ) where xt is ( path-addr path-bytes -- ) .

This matches the glob pattern against the paths of all directories and all files in and in all subdirectories of either the current directory of the current filesystem or the root directory of the current filesystem, depending on whether the glob pattern begins with /. The paths of matches relative to the current directory or starting at the root directory, if the glob pattern began with /, will be individually passed into the executed execution token.

A glob pattern is a path containing any number of wildcards, which are *, which matches any number of arbitrary bytes, and ?`, which matches a single arbitrary byte.

If the glob pattern ends in / only leaves which are directories will be matched (note that the / will be included in the resulting path). Likewise, if the last element of a glob pattern contains . and the element is not . or .. only leaves which are files will be matched. Also, wildcards do not match . or .. to avoid unexpected matches from occurring; however, glob patterns can contain . and ...

Note that a matched path is limited to 256 bytes in length; longer paths are ignored. However, as zeptoforth does not support long file names, and thus file names are limited to 12 bytes in length and directory names are limited to 8 bytes in length, this should not be a practical issue in most use cases. If this is an issue, one can change the current directory prior to using the glob tool to shorten the generated paths.

Also note that globbing cannot be done with words that permanently add new data to the current task’s RAM dictionary, as it uses the current task’s RAM dictionary for scratchpad space. As a result it cannot be used with fat32-tools::included. However, it can be used with words that temporarily store data in the RAM dictionary or which modify variables, values, buffer:s, or like.

Here is a screenshot of globbing in action – forgive the tiny font, I had just reinstalled zeptoforth with my new 4x8-pixel font on my PicoCalc:

And here is a source code listing of the glob tool:

\ Copyright (c) 2026 Travis Bemann
\
\ Permission is hereby granted, free of charge, to any person obtaining a copy
\ of this software and associated documentation files (the "Software"), to deal
\ in the Software without restriction, including without limitation the rights
\ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
\ copies of the Software, and to permit persons to whom the Software is
\ furnished to do so, subject to the following conditions:
\ 
\ The above copyright notice and this permission notice shall be included in
\ all copies or substantial portions of the Software.
\ 
\ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
\ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
\ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
\ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
\ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
\ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
\ SOFTWARE.

continue-module fat32-tools
  
  oo import
  fat32 import

  continue-module fat32-tools-internal
    
    \ Extract an element from a path
    : split-elem
      { addr bytes -- elem-addr elem-bytes rest-addr rest-bytes }
      addr bytes { cur-addr cur-bytes }
      begin cur-bytes 0> while
        cur-addr c@ [char] / <> if
          1 +to cur-addr -1 +to cur-bytes
        else
          cur-bytes 1 > if
            addr bytes cur-bytes - cur-addr 1+ cur-bytes 1- exit
          else
            addr bytes cur-bytes - cur-addr 1 exit
          then
        then
      repeat
      addr bytes 0 0
    ;

    \ Match a string against a glob
    : match-glob? { addr bytes glob-addr glob-bytes -- match? }
      addr bytes glob-addr glob-bytes equal-strings? not if
        addr bytes s" ." equal-strings? if false exit then
        addr bytes s" .." equal-strings? if false exit then
      then
      0 0 0 0 { px nx next-px next-nx }
      begin px glob-bytes < nx bytes < or while
        false { continue? }
        px glob-bytes < if
          glob-addr px + c@ fat32-internal::upcase-char { c }
          c [char] ? = if \ Single-character wildcard
            nx bytes < if
              1 +to px 1 +to nx true to continue?
            then
          else
            c [char] * = if \ Zero-or-greater character wildcard
              \ Try to match at nx.
              \ If that doesn't work out, restart at nx 1+ next.
              px to next-px nx 1+ to next-nx 1 +to px true to continue?
            else
              \ Ordinary character
              nx bytes < if
                addr nx + c@ fat32-internal::upcase-char c = if
                  1 +to px 1 +to nx true to continue?
                then
              then
            then
          then
        then
        continue? not if
          \ Mismatch. Maybe restart.
          next-nx 0> next-nx bytes <= and if
            next-px to px next-nx to nx true to continue?
          then
        then
        continue? not if false exit then
      repeat
      \ Matched all of pattern to all of name. Success.
      true
    ;

    \ Glob frame
    <object> begin-class <glob-frame>

      \ The directory for a frame
      <fat32-dir> class-size member glob-dir

      \ The glob pattern for the frame
      2 cells member glob-pattern

      \ The glob path buffer
      2 cells member glob-buf

      \ The glob path offset
      cell member glob-off

      \ The glob xt
      cell member glob-xt
      
      \ The previous frame
      cell member glob-prev

      \ Entry
      <fat32-entry> class-size member glob-entry
      
      \ Name buffer
      12 cell align member glob-name-buf
      
    end-class

    \ Implement a glob frame
    <glob-frame> begin-implement

      \ Constructor
      :noname { prev xt off D: buf D: pattern dir self -- }
        self <object>->new
        self glob-dir dir clone-dir
        pattern self glob-pattern 2!
        buf self glob-buf 2!
        off self glob-off !
        xt self glob-xt !
        prev self glob-prev !
        <fat32-entry> self glob-entry init-object
      ; define new

      \ Destructor
      :noname { self -- }
        self glob-entry destroy
        self glob-dir close-dir
        self glob-dir destroy
        self <object>->destroy
      ; define destroy
      
    end-implement
    
  end-module> import
  
  \ Execute an xt for each file or directory matching a glob path
  : glob ( addr bytes xt -- )
    256 [: { addr bytes xt buf }
      ram-here { frame }
      <glob-frame> class-size cell align ram-allot
      addr bytes xt buf frame [:
        <fat32-dir> class-size [:
          { addr bytes xt buf frame base-dir }
          0 { off }
          begin-critical
          bytes 0> if
            addr c@ [char] / = if
              base-dir current-fs@ root-dir@ 1 +to addr -1 +to bytes
              [char] / buf c! 1 to off
            else
              base-dir current-fs@ current-dir@
            then
          then
          0 xt off buf off + 256 off -
          addr bytes base-dir <glob-frame> frame init-object
          base-dir close-dir
          base-dir destroy
          addr bytes
        ;] with-aligned-allot
      ;] try
      ?dup if frame ram-here! end-critical ?raise then
      to bytes to addr
      end-critical
      begin frame while
        frame [: { frame }
          frame glob-entry frame glob-dir read-dir if
            frame glob-pattern 2@ split-elem
            { elem-addr elem-bytes rest-addr rest-bytes }
            frame glob-name-buf 12 frame glob-entry file-name@ nip
            { name-bytes }
            frame glob-name-buf name-bytes
            elem-addr elem-bytes match-glob? if
              name-bytes frame glob-buf 2@ nip <= if
                frame glob-name-buf frame glob-buf 2@ drop name-bytes move
                rest-bytes 0> if
                  rest-addr rest-bytes s" /" equal-strings? if
                    name-bytes frame glob-buf 2@ nip < if
                      [char] / frame glob-buf 2@ drop name-bytes + c!
                      frame glob-entry entry-dir? if
                        frame glob-buf 2@ drop
                        frame glob-off @ -
                        frame glob-off @ name-bytes 1+ +
                        frame glob-xt @ execute
                      then
                    then
                    false
                  else
                    name-bytes frame glob-buf 2@ nip < if
                      frame glob-entry entry-dir? if
                        [char] / frame
                        glob-buf 2@ drop name-bytes + c!
                        name-bytes 1+ true
                      else
                        false
                      then
                    else
                        false
                    then
                  then
                else
                  frame glob-entry entry-file?
                  frame glob-entry entry-dir? or if
                    frame glob-buf 2@ drop
                    frame glob-off @ -
                    frame glob-off @ name-bytes +
                    frame glob-xt @ execute
                  then
                  false
                then
              else
                false
              then
            else
              false
            then
            if { name-bytes }
              ram-here { new-frame }
              <glob-frame> class-size cell align ram-allot
              name-bytes frame new-frame <fat32-dir> class-size [:
                [:
                  { name-bytes frame new-frame dir }
                  frame glob-buf 2@ drop name-bytes 1-
                  dir frame glob-dir open-dir
                  name-bytes frame new-frame dir [:
                    { name-bytes frame new-frame dir }
                    frame frame glob-xt @ frame glob-off @ name-bytes +
                    frame glob-buf 2@
                    name-bytes - swap name-bytes + swap
                    frame glob-pattern 2@ split-elem 2nip
                    dir <glob-frame> new-frame init-object
                  ;] try
                  dir close-dir
                  dir destroy
                  ?raise
                ;] with-aligned-allot
              ;] critical
              new-frame
            else
              frame
            then
          else
            frame glob-prev @ { prev-frame }
            frame destroy
            frame ram-here!
            prev-frame
          then
        ;] try
        dup if
          begin frame while
            frame glob-prev @ { prev-frame }
            frame destroy
            frame ram-here!
            prev-frame to frame
            frame if frame <glob-frame> class-size + ram-here! then
          repeat
        else
          swap to frame
          frame if frame <glob-frame> class-size + ram-here! then
        then
        ?raise
      repeat
    ;] with-allot
  ;
  
end-module
1 Like

zeptoforth 1.16.5 is out!

You can get this release from Release Patch-level release 1.16.5 · tabemann/zeptoforth · GitHub.

This release:

  • increases the return stack size of the main task from 1 KiB to 2 KiB.
  • removes an unnecessary instruction from if, while, and until, resulting in smaller and faster code.
  • adds an optional ‘glob’ tool in extra/common/glob.fs.
  • adds a 4x8-pixel font, which can be used with the PicoCalc amongst other things.
3 Likes