{"id":13760553,"url":"https://github.com/vindarel/cl-str","last_synced_at":"2026-02-19T19:32:28.548Z","repository":{"id":15582636,"uuid":"78425658","full_name":"vindarel/cl-str","owner":"vindarel","description":"Modern, simple and consistent Common Lisp string manipulation library.","archived":false,"fork":false,"pushed_at":"2025-09-05T13:55:10.000Z","size":212,"stargazers_count":334,"open_issues_count":16,"forks_count":37,"subscribers_count":9,"default_branch":"master","last_synced_at":"2026-02-06T13:48:16.686Z","etag":null,"topics":["common-lisp","quicklisp","string-manipulation"],"latest_commit_sha":null,"homepage":"https://vindarel.github.io/cl-str/","language":"Common Lisp","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/vindarel.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":".github/FUNDING.yml","license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"github":["vindarel"],"ko_fi":"vindarel"}},"created_at":"2017-01-09T12:12:17.000Z","updated_at":"2026-01-30T05:01:11.000Z","dependencies_parsed_at":"2023-12-29T02:42:07.175Z","dependency_job_id":"3a9b72f9-8dd1-4840-88d0-731670469944","html_url":"https://github.com/vindarel/cl-str","commit_stats":null,"previous_names":[],"tags_count":17,"template":false,"template_full_name":null,"purl":"pkg:github/vindarel/cl-str","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vindarel%2Fcl-str","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vindarel%2Fcl-str/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vindarel%2Fcl-str/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vindarel%2Fcl-str/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/vindarel","download_url":"https://codeload.github.com/vindarel/cl-str/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/vindarel%2Fcl-str/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":29628808,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-02-19T18:02:07.722Z","status":"ssl_error","status_checked_at":"2026-02-19T18:01:46.144Z","response_time":117,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.6:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["common-lisp","quicklisp","string-manipulation"],"created_at":"2024-08-03T13:01:12.753Z","updated_at":"2026-02-19T19:32:28.526Z","avatar_url":"https://github.com/vindarel.png","language":"Common Lisp","readme":"[![Quicklisp](http://quickdocs.org/badge/cl-str.svg)](http://quickdocs.org/cl-str)\n# A modern and consistent Common Lisp string manipulation library\n\n    (ql:quickload \"str\")\n\nalso on [Ultralisp](http://ultralisp.org/), [ocicl](https://github.com/ocicl/ocicl/) and [vend](https://github.com/fosskers/vend/).\n\nWhy ?\n\n* modernity, simplicity and discoverability:\n\n  - `(str:trim s)` instead of `(string-trim '(#\\Backspace #\\Tab #\\Linefeed #\\Newline #\\Vt #\\Page #\\Return #\\Space #\\Rubout #\\Next-Line #\\No-break_space) s))`,\nor `str:concat strings` instead of an unusual `format` construct; one discoverable library instead of many;\n\n* consistence and composability, where `s` is always the last argument, which makes it\n  easier to feed pipes and arrows.\n\n* fixing built-in surprises: `(string-downcase nil`)  =\u003e `\"nil\"` the string, whereas `(str:downcase nil)` =\u003e `nil`.\n\n\nThe only dependency is `cl-ppcre`.\n\n\u003c!-- markdown-toc start - Don't edit this section. Run M-x markdown-toc-refresh-toc --\u003e\n**Table of Contents**\n\n- [A modern and consistent Common Lisp string manipulation library](#a-modern-and-consistent-common-lisp-string-manipulation-library)\n    - [Install](#install)\n    - [Global parameters](#global-parameters)\n    - [Functions](#functions)\n        - [Tweak whitespace](#tweak-whitespace)\n            - [trim `(s \u0026key (char-bag *whitespaces*))`](#trim-s-key-char-bag-whitespaces)\n            - [collapse-whitespaces `(s)`](#collapse-whitespaces-s)\n        - [To longer strings](#to-longer-strings)\n            - [join `(separator list-of-strings)`](#join-separator-list-of-strings)\n            - [concat `(\u0026rest strings)`](#concat-rest-strings)\n            - [ensure `(s \u0026key wrapped-in prefix suffix)` NEW in March, 2023](#ensure-s-key-wrapped-in-prefix-suffix-new-in-march-2023)\n            - [ensure-prefix, ensure-suffix `(start/end s)` NEW in March, 2023](#ensure-prefix-ensure-suffix-startend-s-new-in-march-2023)\n            - [ensure-wrapped-in `(start/end s)`](#ensure-wrapped-in-startend-s)\n            - [insert `(string/char index s)`](#insert-stringchar-index-s)\n            - [repeat `(count s)`](#repeat-count-s)\n            - [add-prefix, add-suffix `(items s)`](#add-prefix-add-suffix-items-s)\n            - [pad `(len s \u0026key (pad-side :right) (pad-char #\\Space))`, pad-left, pad-right, pad-center (new in 0.16, 2019/12)](#pad-len-s-key-pad-side-right-pad-char-space-pad-left-pad-right-pad-center-new-in-016-201912)\n        - [To shorter strings](#to-shorter-strings)\n            - [substring `(start end s)`](#substring-start-end-s)\n            - [s-first `(s)`](#s-first-s)\n            - [s-last `(s)`](#s-last-s)\n            - [s-rest `(s)`](#s-rest-s)\n            - [s-nth `(n s)`](#s-nth-n-s)\n            - [shorten `(len s \u0026key ellipsis)`](#shorten-len-s-key-ellipsis)\n        - [To a fixed length](#to-a-fixed-length)\n            - [fit `(len s)`](#fit-len-s)\n        - [To and from lists](#to-and-from-lists)\n            - [words `(s)`](#words-s)\n            - [unwords `(strings)`](#unwords-strings)\n            - [lines `(s \u0026key omit-nulls)`](#lines-s-key-omit-nulls)\n            - [unlines `(strings)`](#unlines-strings)\n            - [split `(separator s \u0026key omit-nulls limit start end regex)`](#split-separator-s-key-omit-nulls-limit-start-end-regex)\n            - [rsplit `(separator s \u0026key limit regex)`](#rsplit-separator-s-key-limit-regex)\n            - [split-omit-nulls `(separator s \u0026key regex)`](#split-omit-nulls-separator-s-key-regex)\n        - [To and from files](#to-and-from-files)\n            - [from-file `(filename)`](#from-file-filename)\n            - [to-file `(filename s)`](#to-file-filename-s)\n        - [Predicates](#predicates)\n            - [emptyp `(s)`](#emptyp-s)\n            - [blankp `(s)`](#blankp-s)\n            - [starts-with-p `(start s \u0026key ignore-case)`](#starts-with-p-start-s-key-ignore-case)\n            - [ends-with-p `(end s \u0026key ignore-case)`](#ends-with-p-end-s-key-ignore-case)\n            - [containsp `(substring s \u0026key (ignore-case nil))`](#containsp-substring-s-key-ignore-case-nil)\n            - [s-member `(list s \u0026key (ignore-case *ignore-case*) (test #'string=))`](#s-member-list-s-key-ignore-case-ignore-case-test-string)\n            - [prefixp and suffixp `(items s)`](#prefixp-and-suffixp-items-s)\n            - [wrapped-in-p (`start/end` `s`) NEW in March, 2023](#wrapped-in-p-startend-s-new-in-march-2023)\n        - [Case](#case)\n            - [Functions to change case: camel-case, snake-case,...](#functions-to-change-case-camel-case-snake-case)\n            - [downcase, upcase, capitalize `(s)` fixing a built-in suprise.](#downcase-upcase-capitalize-s-fixing-a-built-in-suprise)\n            - [downcasep, upcasep `(s)`](#downcasep-upcasep-s)\n            - [alphap, lettersp `(s)`](#alphap-lettersp-s)\n            - [alphanump, lettersnump `(s)`](#alphanump-lettersnump-s)\n            - [ascii-p `(char/s)`](#ascii-p-chars)\n            - [digitp `(s)`](#digitp-s)\n            - [has-alpha-p, has-letters-p, has-alphanum-p `(s)`](#has-alpha-p-has-letters-p-has-alphanum-p-s)\n        - [Others](#others)\n            - [replace-first `(old new s \u0026key regex)`](#replace-first-old-new-s-key-regex)\n            - [replace-all `(old new s \u0026key regex)`](#replace-all-old-new-s-key-regex)\n            - [replace-using `(replacement-list s \u0026key regex)`](#replace-using-replacement-list-s-key-regex)\n            - [remove-punctuation (s \u0026key replacement)](#remove-punctuation-s-key-replacement)\n            - [prefix `(list-of-strings)` (renamed in 0.9)](#prefix-list-of-strings-renamed-in-09)\n            - [suffix `(list-of-strings)`](#suffix-list-of-strings)\n            - [count-substring `(substring s \u0026key start end)`](#count-substring-substring-s-key-start-end)\n            - [s-assoc-value `(alist key)`](#s-assoc-value-alist-key)\n    - [Macros](#macros)\n        - [string-case](#string-case)\n        - [match (experimental) · new in Feb, 2024](#match-experimental--new-in-feb-2024)\n    - [Changelog](#changelog)\n    - [Dev and test](#dev-and-test)\n        - [Main test suite](#main-test-suite)\n        - [Specific test suite](#specific-test-suite)\n        - [Specific test](#specific-test)\n        - [Test when defined](#test-when-defined)\n    - [See also](#see-also)\n\n\u003c!-- markdown-toc end --\u003e\n\n## Install\n\nInstall with [Quicklisp](https://www.quicklisp.org/beta/):\n\n    (ql:quickload :str)\n\nAdd it in your .asd's project dependencies, and call functions with the `str` prefix. It is not recommended to `:use :str` in a package. It's safer to use the `str` prefix.\n\nCheck its version:\n\n    (str:version)\n\nTo get a newer version, you need to update the Quicklisp dist (think\nof QL as Debian's apt rather than pip/npm/etc):\n\n    (ql:update-dist \"quicklisp\")\n\nDon't have a full Common Lisp development environment yet ? Get\n[Portacle](https://shinmera.github.io/portacle/), a portable and\nmultiplatform development environment shipping Emacs, Quicklisp, SBCL\nand Git. See also [editor\nsupport](https://lispcookbook.github.io/cl-cookbook/editor-support.html)\n(Vim, Lem, Atom, Eclipse,…).\n\n## Global parameters\n\nSome parameters are common to various functions and often used:\n`:ignore-case` and `:omit-nulls`.\n\nConsequently we can also manage them with global parameters:\n\n~~~lisp\n(let ((str:*ignore-case* t))\n  (str:ends-with-p \"BAR\" \"foobar\"))\n~~~\n\nis equivalent to\n\n~~~lisp\n(str:ends-with-p \"BAR\" \"foobar\" :ignore-case t)\n~~~\n\n## Functions\n\n### Tweak whitespace\n\n#### trim `(s \u0026key (char-bag *whitespaces*))`\n\nRemoves all characters in `char-bag` (default: whitespaces) at the beginning and end of `s`.\nIf supplied, `char-bag` has to be a sequence (e.g. string or list of characters).\n\n```lisp\n(str:trim \"  rst  \") ;; =\u003e \"rst\"\n(str:trim \"+-*foo-bar*-+\" :char-bag \"+-*\") =\u003e \"foo-bar\"\n(str:trim \"afood\" :char-bag (concat \"a\" \"d\")) =\u003e \"foo\"\"\n(str:trim \"cdoooh\" :char-bag (str:concat \"c\" \"d\" \"h\")) =\u003e \"ooo\"\n```\n\nAlso `trim-left` and `trim-right`.\n\nUses the built-in\n[string-trim](https://lispcookbook.github.io/cl-cookbook/strings.html#trimming-blanks-from-the-ends-of-a-string)\nwhere whitespaces are `'(#\\Space #\\Newline #\\Backspace #\\Tab #\\Linefeed #\\Page #\\Return #\\Rubout)`.\n\n#### collapse-whitespaces `(s)`\n\nEnsure there is only one space character between words. Remove newlines.\n\n~~~lisp\n(str:collapse-whitespaces \"foo  bar\n\n\n  baz\")\n;; \"foo bar baz\"\n;;T\n~~~\n\n\n### To longer strings\n\n#### join `(separator list-of-strings)`\n\nJoin strings in list `list-of-strings` with `separator` (either a string or a char) in between.\n\n```lisp\n(join \" \" '(\"foo\" \"bar\" \"baz\")) ;; =\u003e \"foo bar baz\"\n(join #\\Space '(\"foo\" \"bar\" \"baz\")) ;; =\u003e \"foo bar baz\"\n```\n\n#### concat `(\u0026rest strings)`\n\nJoin strings into one.\n\n```lisp\n(concat \"f\" \"o\" \"o\") ;; =\u003e \"foo\"\n```\n\nSimple call of the built-in [concatenate](https://lispcookbook.github.io/cl-cookbook/strings.html#concatenating-strings).\n\nWe actually also have `uiop:strcat`.\n\n#### ensure `(s \u0026key wrapped-in prefix suffix)` NEW in March, 2023\n\nThe \"ensure-\" functions return a string that has the specified prefix or suffix, appended if necessary.\n\nThis `str:ensure` function looks for the following key parameters, in order:\n\n- `:wrapped-in`: if non nil, call `str:ensure-wrapped-in`. This checks that `s` both starts and ends with the supplied string or character.\n- `:prefix` and `:suffix`: if both are supplied and non-nil, call `str:ensure-suffix` followed by `str:ensure-prefix`.\n- `:prefix`: call `str:ensure-prefix`\n- `:suffix`: call `str:ensure-suffix`.\n\nExample:\n\n~~~lisp\n(str:ensure \"abc\" :wrapped-in \"/\")  ;; =\u003e \"/abc/\"\n(str:ensure \"/abc\" :prefix \"/\")  ;; =\u003e \"/abc\"  =\u003e no change, still one \"/\"\n(str:ensure \"/abc\" :suffix \"/\")  ;; =\u003e \"/abc/\" =\u003e added a \"/\" suffix.\n~~~\n\nThese functions accept strings and characters:\n\n~~~lisp\n(str:ensure \"/abc\" :prefix #\\/)\n~~~\n\nwarn: if both `:wrapped-in` and `:prefix` (and/or `:suffix`) are supplied together, `:wrapped-in` takes precedence and `:prefix` (and/or `:suffix`) is ignored.\n\n\n#### ensure-prefix, ensure-suffix `(start/end s)` NEW in March, 2023\n\nEnsure that `s` starts with `start/end` (or ends with `start/end`, respectively).\n\nReturn a new string with its prefix (or suffix) added, if necessary.\n\nExample:\n\n~~~lisp\n(str:ensure-prefix \"/\" \"abc/\") =\u003e \"/abc/\" (a prefix was added)\n;; and\n(str:ensure-prefix \"/\" \"/abc/\") =\u003e \"/abc/\" (does nothing)\n~~~\n\n#### ensure-wrapped-in `(start/end s)`\n\nEnsure that `s` both starts and ends with `start/end`.\n\nReturn a new string with the necessary added bits, if required.\n\nIt simply calls `str:ensure-suffix` followed by `str:ensure-prefix`.\n\nSee also `str:wrapped-in-p` and `uiop:string-enclosed-p prefix s suffix`.\n\n~~~lisp\n(str:ensure-wrapped-in \"/\" \"abc\") ;; =\u003e \"/abc/\"  (added both a prefix and a suffix)\n(str:ensure-wrapped-in \"/\" \"/abc/\") ;; =\u003e \"/abc/\" (does nothing)\n~~~\n\n#### insert `(string/char index s)`\n\nInsert the given string (or character) at the index `index` into `s` and return a\nnew string.\n\nIf `index` is out of bounds, just return `s`.\n\n```lisp\n(str:insert \"l\" 2 \"helo\") ; =\u003e \"hello\"\n\n(str:insert \"o\" 99 \"hell\") : =\u003e \"hell\"\n```\n\n#### repeat `(count s)`\n\nMake a string of `s` repeated `count` times.\n\n```lisp\n(repeat 3 \"foo\") ;; =\u003e \"foofoofoo\"\n```\n\n**NEW** as of April, 2025: when the expected output is more than 10,000\ncharacters, we generate it in chunks to avoid stack/heap overflows.\n\n#### add-prefix, add-suffix `(items s)`\n\nRespectively prepend or append `s` to the front of each item.\n\n\n#### pad `(len s \u0026key (pad-side :right) (pad-char #\\Space))`, pad-left, pad-right, pad-center (new in 0.16, 2019/12)\n\nFill `s` with characters until it is of the given length. By default,\nadd spaces on the right:\n\n~~~lisp\n(str:pad 10 \"foo\")\n\"foo       \"\n~~~\n\n* `pad-side`: one of `:right` (the default), `:left` or `:center`. See `*pad-side*`.\n* `pad-char`: the padding character (or string of one character). Defaults to a space. See `*pad-char*`.\n\n~~~lisp\n(str:pad 10 \"foo\" :pad-side :center :pad-char \"+\")\n\"+++foo++++\"\n~~~\n\nIf the given length is smaller than the length o `s`, return `s`.\n\nFilling with spaces can easily be done with format:\n\n~~~lisp\n(format nil \"~va\" len s) ;; =\u003e \"foo       \"\n(format nil \"~v@a\" 10 \"foo\") ;; =\u003e \"       foo\" (with @)\n~~~\n\n\n### To shorter strings\n\n#### substring `(start end s)`\n\nReturn the substring of `s` from `start` to `end`.\n\nIt uses `subseq` with differences:\n\n* argument order, s at the end\n* `start` and `end` can be lower than 0 or bigger than the length of s.\n* for convenience `end` can be nil or t to denote the end of the string.\n\nExamples:\n\n```lisp\n  (is \"abcd\" (substring 0 t \"abcd\") \"t denotes the end of the string\")\n  (is \"abcd\" (substring 0 nil \"abcd\") \"nil too\")\n  (is \"abcd\" (substring 0 100 \"abcd\") \"end can be too large\")\n  (is \"abc\" (substring 0 -1 \"abcd\") \"end can be negative. Counts from the end.\")\n  (is \"\" (substring 0 -100 \"abcd\") \"end can be negative and too low\")\n  (is \"\" (substring 100 1 \"abcd\") \"start can be too big\")\n  (is \"abcd\" (substring -100 4 \"abcd\") \"start can also be too low\")\n  (is \"\" (substring 2 1 \"abcd\") \"start is bigger than end\")\n```\n\n#### s-first `(s)`\n\nReturn the first letter of `s`.\n\n\nExamples:\n\n```lisp\n  (s-first \"foobar\") ;; =\u003e \"f\"\n  (s-first \"\") ;; =\u003e \"\"\n```\n\n#### s-last `(s)`\n\nReturn the last letter of `s`.\n\n\n#### s-rest `(s)`\n\nReturn the rest substring of `s`.\n\nExamples:\n\n```lisp\n  (s-rest \"foobar\") ;; =\u003e \"oobar\"\n  (s-rest \"\") ;; =\u003e \"\"\n```\n\n#### s-nth `(n s)`\n\nReturn the nth letter of `s`.\n\nExamples:\n\n```lisp\n  (s-nth 3 \"foobar\") ;; =\u003e \"b\"\n  (s-nth 3 \"\") ;; =\u003e \"\"\n```\n\nYou could also use\n\n~~~lisp\n(elt \"test\" 1)\n;; =\u003e #\\e\n(string (elt \"test\" 1))\n;; =\u003e \"e\"\n~~~\n\n#### shorten `(len s \u0026key ellipsis)`\n\nIf `s` is longer than `len`, truncate it and add an ellipsis at the\nend (`...` by default). `s` is cut down to `len` minus the length of\nthe ellipsis (3 by default).\n\nOptionally, give an `:ellipsis` keyword argument. Also set it globally\nwith `*ellipsis*`.\n\n~~~lisp\n(shorten 8 \"hello world\")\n;; =\u003e \"hello...\"\n(shorten 3 \"hello world\")\n;; =\u003e \"...\"\n(shorten 8 \"hello world\" :ellipsis \"-\")\n;; =\u003e \"hello w-\"\n(let ((*ellipsis* \"-\"))\n  (shorten 8 \"hello world\"))\n;; =\u003e \"hello w-\"\n~~~\n\n### To a fixed length\n\n#### fit `(len s)`\n\nFit this string to the given length:\n\n- if it's too long, shorten it (showing the `ellipsis`),\n- if it's too short, add paddding (to the side `pad-side`, adding the\n  character `pad-char`).\n\nAs such, it accepts the same key arguments as `str:shorten` and\n`str:pad`: `ellipsis`, `pad-side`, `pad-char`…\n\n~~~lisp\nCL-USER\u003e (str:fit 10 \"hello\" :pad-char \"+\")\n\"hello+++++\"\n\nCL-USER\u003e (str:fit 10 \"hello world\" :ellipsis \"…\")\n\"hello wor…\"\n~~~\n\nIf, like me, you want to print a list of data as a table, see:\n\n- [cl-ansi-term](https://github.com/vindarel/cl-ansi-term/)\n\n~~~lisp\nCL-USER\u003e (ql:quickload \"cl-ansi-term\")\nCL-USER\u003e (term:table '((\"name\" \"age\" \"email\")\n              (\"me\" 7 \"some@blah\")\n              (\"me\" 7 \"some@with-some-longer.email\"))\n             :column-width '(10 4 20))\n+---------+---+-------------------+\n|name     |age|email              |\n+---------+---+-------------------+\n|me       |7  |some@blah          |\n+---------+---+-------------------+\n|me       |7  |some@with-some-l(…)|\n+---------+---+-------------------+\n~~~\n\n- [cl-ascii-table](https://github.com/telephil/cl-ascii-table/)\n\n~~~lisp\nCL-USER\u003e (ql:quickload \"cl-ascii-table\")\nCL-USER\u003e (let ((table (ascii-table:make-table '(\"Id\" \"Name\" \"Amount\") :header \"Infos\")))\n  (ascii-table:add-row table '(1 \"Bob\" 150))\n  (ascii-table:add-row table '(2 \"Joe\" 200))\n  (ascii-table:add-separator table)\n  (ascii-table:add-row table '(\"\" \"Total\" 350))\n  (ascii-table:display table))\n\n.---------------------.\n|        Infos        |\n+----+-------+--------+\n| Id | Name  | Amount |\n+----+-------+--------+\n|  1 | Bob   |    150 |\n|  2 | Joe   |    200 |\n+----+-------+--------+\n|    | Total |    350 |\n+----+-------+--------+\nNIL\n~~~\n\n\n### To and from lists\n\n#### words `(s)`\n\nReturn list of words, which were delimited by whitespace.\n\n#### unwords `(strings)`\n\nJoin the list of strings with a whitespace.\n\n#### lines `(s \u0026key omit-nulls)`\n\nSplit string by newline character and return list of lines.\n\nA terminal newline character does *not* result in an extra empty string\n(new in **v0.14**, october 2019).\n\n\n#### unlines `(strings)`\n\nJoin the list of strings with a newline character.\n\n#### paragraphs, unparagraphs\n\nSplit the string by \\n\\n: paragraphs are sections of text separated by a blank line\n(two #\\Newline characters in a row).\n\nReturn a list of strings.\n\nEach paragraph has whitespace strimmed around it. As such, the\noperation `(unparagraphs (paragraphs s))` doesn't always re-create\n`s`, it creates a new string with less blank lines.\n\nEquivalent to `ppcre:split \"\\\\n\\\\n\" s`, plus trimming whitespace on the results.\n\nThe `unparagraphs` functions joins the list of strings by a blank line.\n\n\n#### split `(separator s \u0026key omit-nulls limit start end regex)`\n\nSplit into subtrings. If\n`omit-nulls` is non-nil, zero-length substrings are omitted.\n\nBy default, metacharacters are treated as normal characters.\n If `regex` is not `nil`, then `separator` is treated as regular expression.\n\n```lisp\n(split \"+\" \"foo++bar\") ;; =\u003e (\"foo\" \"\" \"bar\")\n(split #\\+ \"foo++bar\") ;; =\u003e (\"foo\" \"\" \"bar\")\n(split \"+\" \"foo++bar\" :omit-nulls t) ;; =\u003e (\"foo\" \"bar\")\n\n(split \"[,|;]\" \"foo,bar;baz\") ;; =\u003e (\"foo,bar;baz\")\n(split \"[,|;]\" \"foo,bar;baz\" :regex t) ;; =\u003e (\"foo\" \"bar\" \"baz\")\n```\n\ncl-ppcre has an inconsistency such that when the separator appears at\nthe end, it doesn't return a trailing empty string. But we do **since\nv0.14** (october, 2019).\n\n#### rsplit `(separator s \u0026key limit regex)`\n\nSimilar to `split`, but split from the end. In particular, this will\nbe different from `split` when a `:limit` is provided, but in more\nobscure cases it can be different when there are multiple different\nways to split the string.\n\n```lisp\n(rsplit \"/\" \"/var/log/mail.log\" :limit 2) ;; =\u003e (\"/var/log\" \"mail.log\")\n```\n\n~~~lisp\n(cl-ppcre:split \" \" \"a b c \")\n(\"a\" \"b\" \"c\")\n\n(str:split \" \" \"a b c \")\n(\"a\" \"b\" \"c\" \"\")\n~~~\n\n\n#### split-omit-nulls `(separator s \u0026key regex)`\n\nBecause it is a common pattern and it can be clearer than an option\ncoming after many parenthesis.\n\n\n### To and from files\n\n#### from-file `(filename)`\n\nRead the file and return its content as a string.\n\nExample: `(str:from-file \"path/to/file.txt\")`.\n\n`:external-format`: if nil, the system default. Can be bound to `:utf-8`.\n\nBut you might just call\n[uiop's `uiop:read-file-string`](https://github.com/fare/asdf/blob/master/uiop/stream.lisp#L445)\ndirectly.\n\nThere is also `uiop:read-file-lines`.\n\n#### to-file `(filename s)`\n\nWrite the string `s` to the file `filename`. If the file does not\nexist, create it, if it already exists, replace it.\n\nOptions:\n\n* `:if-does-not-exist`: `:create` (default), `:error`\n* `:if-exists`: `:supersede` (default), `:append`, `:overwrite`, `:rename`, `:error`,...\n\nReturns the string written to file.\n\n\n### Predicates\n\n#### emptyp `(s)`\n\nTrue if `s` is nil or the empty string:\n\n```lisp\n  (emptyp nil) ;; =\u003e T\n  (emptyp \"\")  ;; =\u003e T\n  (emptyp \" \") ;; =\u003e NIL\n```\n\nSee also `str:non-empty-string-p`, which adds a `stringp` check.\n\n#### blankp `(s)`\n\nTrue if `s` is empty or only contains whitespaces.\n\n    (blankp \"\") ;; =\u003e T\n    (blankp \" \") ;; =\u003e T\n    (emptyp \" \") ;; =\u003e NIL\n\nSee also `str:non-blank-string-p`.\n\n#### starts-with-p `(start s \u0026key ignore-case)`\n\nTrue if `s` starts with the substring `start`, nil otherwise. Ignore\ncase by default.\n\n    (starts-with-p \"foo\" \"foobar\") ;; =\u003e T\n    (starts-with-p \"FOO\" \"foobar\") ;; =\u003e NIL\n    (starts-with-p \"FOO\" \"foobar\" :ignore-case t) ;; =\u003e T\n\nCalls `string=` or `string-equal` depending on the case, with their\n`:start` and `:end` delimiters.\n\n#### ends-with-p `(end s \u0026key ignore-case)`\n\nTrue if `s` ends with the substring `end`. Ignore case by default.\n\n    (ends-with-p \"bar\" \"foobar\") ;; =\u003e T\n\n`end` can be a string or a character.\n\n#### containsp `(substring s \u0026key (ignore-case nil))`\n\nReturn true if `s` contains `substring`, nil otherwise. Ignore the\ncase with `:ignore-case t` (don't ignore by default).\n\nBased on a simple call to the built-in `search` (which returns the\nposition of the substring).\n\n#### s-member `(list s \u0026key (ignore-case *ignore-case*) (test #'string=))`\n\nReturn T if `s' is a member of `list'. Do not ignore case by default.\n\nNOTE: `s-member`'s arguments' order is the reverse of CL's `member`.\n\nIf `:ignore-case` or `*ignore-case*` are not nil, ignore case (using\n`string-equal` instead of `string=`).\n\nUnlike CL's `member`, `s-member` returns T or NIL, instead of the tail of LIST whose first element satisfies the test.\n\n\n#### prefixp and suffixp `(items s)`\n\nReturn `s` if all `items` start (or end) with it.\n\nSee also `uiop:string-prefix-p prefix s`, which returns `t` if\n`prefix` is a prefix of `s`,\n\nand `uiop:string-enclosed-p prefix s suffix`, which returns `t` if `s`\nbegins with `prefix` and ends with `suffix`.\n\n#### wrapped-in-p (`start/end` `s`) NEW in March, 2023\n\nDoes `s` start and end with `start/end'?\n\nIf true, return `s`. Otherwise, return nil.\n\nExample:\n\n~~~lisp\n(str:wrapped-in-p \"/\" \"/foo/\"  ;; =\u003e \"/foo/\"\n(str:wrapped-in-p \"/\" \"/foo\"  ;; =\u003e nil\n~~~\n\nSee also: `UIOP:STRING-ENCLOSED-P (prefix s suffix)`.\n\n\n### Case\n\n#### Functions to change case: camel-case, snake-case,...\n\nWe use\n[cl-change-case](https://github.com/rudolfochrist/cl-change-case/) (go\nthank him and star the repo!).\nWe adapt these functions to also accept symbols and characters (like the inbuilt casing functions).\nAlso the functions return `nil` when argument is `nil`.\n\nThe available functions are:\n\n```\n:no-case (s \u0026key replacement)\n:camel-case (s \u0026key merge-numbers)\n:dot-case\n:header-case\n:param-case\n:pascal-case\n:path-case\n:sentence-case\n:snake-case\n:swap-case\n:title-case\n:constant-case\n```\n\nMore documentation and examples are there.\n\n\n#### downcase, upcase, capitalize `(s)` fixing a built-in suprise.\n\nThe functions `str:downcase`, `str:upcase` and `str:capitalize` return\na new string. They call the built-in `string-downcase`,\n`string-upcase` and `string-capitalize` respectively, but they fix\nsomething surprising. When the argument is `nil`, the built-ins return\n\"nil\" or \"NIL\" or \"Nil\", a *string*. Indeed, they work on anything:\n\n    (string-downcase nil) ;; =\u003e \"nil\" the string !\n    (str:downcase nil) ;; nil\n\n    (string-downcase :FOO) ;; =\u003e \"foo\"\n\n#### downcasep, upcasep `(s)`\n\nThese functions return `t` if the given string contains at least one\nletter and all its letters are lowercase or uppercase, respectively.\n\n```lisp\n(is (downcasep \" a+,. \") t \"downcasep with one letter and punctuation is true.\")\n(is (downcasep \" +,. \") nil \"downcasep with only punctuation or spaces is false\")\n```\n\n#### alphap, lettersp `(s)`\n\n`alphap` returns t if `s` contains at least one character and all characters are\n  alpha (as in `\"^[a-zA-Z]+$\"`).\n\n`lettersp` works for unicode letters too.\n\n~~~lisp\n(is (alphap \"abcdeé\") nil \"alphap is nil with accents\")\n(is (lettersp \"éß\") t \"lettersp is t with accents and ß\")\n~~~\n\n#### alphanump, lettersnump `(s)`\n\n`alphanump` returns t if `s` contains at least one character and all characters are alphanumeric (as in `^[a-zA-Z0-9]+$`).\n\n`lettersnump` also works on unicode letters (as in `^[\\\\p{L}a-zA-Z0-9]+$`).\n\n#### ascii-p `(char/s)`\n\nReturn t if the character / string is an ASCII character / is composed of ASCII characters.\n\nAn ASCII character has a `char-code` inferior to 128.\n\n#### digitp `(s)`\n\nReturns t if `s` contains at least one character and all characters are numerical (as for `digit-char-p`).\n\n#### has-alpha-p, has-letters-p, has-alphanum-p `(s)`\n\nReturn t if `s` has at least one alpha, letter, alphanum character (as with `alphanumericp`).\n\n### Others\n\n#### replace-first `(old new s \u0026key regex)`\n\nReplace the first occurence of `old` by `new` in `s`.\n\nBy default, metacharacters are treated as normal characters.\nIf `regex` is not `nil`, then `old` is treated as regular expression.\n\n```lisp\n(replace-first \"a\" \"o\" \"faa\") ;; =\u003e \"foa\"\n(replace-first \"fo+\" \"frob\" \"foofoo bar\" :regex t) ;; =\u003e \"frobfoo bar\"\n```\n\nUses\n[cl-ppcre:regex-replace](http://weitz.de/cl-ppcre/#regex-replace)\nbut quotes the user input to not treat it as a regex (if regex is nil).\n\n#### replace-all `(old new s \u0026key regex)`\n\nReplace all occurences of `old` by `new` in `s`.\n\nBy default, metacharacters are treated as normal characters.\nIf `regex` is not `nil`, `old` is treated as regular expression.\n\n```lisp\n(replace-all \"a\" \"o\" \"faa\") ;; =\u003e \"foo\"\n(replace-all \"fo+\" \"frob\" \"foofoo bar\" :regex t) ;; =\u003e \"frobfrob bar\"\n```\n\nUses\n[cl-ppcre:regex-replace-all](http://weitz.de/cl-ppcre/#regex-replace-all)\nbut quotes the user input to not treat it as a regex (if regex is nil).\n\nIf the replacement is only one character, you can use `substitute`:\n\n    (substitute #\\+ #\\Space \"foo bar baz\")\n    ;; \"foo+bar+baz\"\n\n\n#### replace-using `(replacement-list s \u0026key regex)`\n\nReplace all associations given by pairs in a replacement-list and return a new string.\n\nThe `replacement-list` alternates a string to replace (case sensitive) and its replacement.\nBy default, metacharacters in the string to replace are treated as normal characters.\nIf `regex` is not `nil`, strings to replace are treated as regular expression.\n\nExample:\n\n```lisp\n(replace-using (list \"%phone%\" \"987\")\n               \"call %phone%\")\n;; =\u003e \"call 987\"\n\n(replace-using (list \"fo+\" \"frob\"\n                       \"ba+\" \"Bob\")\n                 \"foo bar\"\n                 :regex t)\n;; =\u003e \"frob Bobr\"\n```\n\n#### remove-punctuation (s \u0026key replacement)\n\nRemove the punctuation characters from `s`, replace them with\n`replacement` (defaults to a space) and strip continuous whitespace.\n\n~~~lisp\n(str:remove-punctuation \"I say: - 'Hello, world?'\") ;; =\u003e \"I say Hello world\"\n~~~\n\nUse `str:no-case` to remove punctuation and return the string as lower-case.\n\n\n#### prefix `(list-of-strings)` (renamed in 0.9)\n\n(renamed from `common-prefix` in v0.9)\n\nFind the common prefix between strings.\n\nExample: `(str:prefix '(\\\"foobar\\\" \\\"foozz\\\"))` =\u003e \\\"foo\\\"\n\nUses the built-in `mismatch`, that returns the position at which\nthe strings fail to match.\n\nReturn a string or nil when the input is the void list.\n\n\n#### suffix `(list-of-strings)`\n\nFind the common suffix between strings.\n\n#### count-substring `(substring s \u0026key start end)`\nCounts the non-overlapping occurrences of `substring` in `s`.\nYou could also count only the ocurrencies between `start` and `end`.\n\nExamples:\n~~~ lisp\n(count-substring \"abc\" \"abcxabcxabc\")\n;; =\u003e 3\n~~~\n~~~lisp\n(count-substring \"abc\" \"abcxabcxabc\" :start 3 :end 7)\n;; =\u003e 1\n~~~\n\n#### s-assoc-value `(alist key)`\n\nReturns the value of a cons cell in `alist` with key `key`, when `key` is a string.\nThe second return value is the cons cell, if any was matched.\n\nThe arguments are in the opposite order of `cl:assoc`'s, but are consistent\nwith `alexandria:assoc-value` (and `str`).\n\n```lisp\n(s-assoc-value '((\"hello\" . 1)) \"hello\")\n;; 1\n;; (\"hello\" . 1)\n\n(alexandria:assoc-value '((\"hello\" . 1)) \"hello\")\n;; NIL\n(alexandria:assoc-value '((\"hello\" . 1)) \"hello\" :test #'string=)\n;; 1\n;; (\"hello\" . 1)\n\n(assoc \"hello\" '((\"hello\" . 1)))\n;; NIL\n(assoc \"hello\" '((\"hello\" . 1)) :test #'string=)\n;; (\"hello\" . 1)\n(cdr *)\n;; 1\n```\n\n## Macros\n\n### string-case\n\nA case-like macro that works with strings (CL case's test function is\n`eql`, and that isn't enough for strings).\n\nExample:\n\n~~~lisp\n(str:string-case \"hello\"\n  (\"foo\" 1)\n  ((\"hello\" \"test\") 5)\n  (nil (print \"input is nil\"))\n  (otherwise (print \"non of the previous forms was caught.\")))\n~~~\n\nYou might also like pattern matching. The example below with\n[trivia](https://github.com/guicho271828/trivia/) is very similar:\n\n~~~lisp\n(trivia:match \"hey\"\n  (\"hey\" (print \"it matched\"))\n  (otherwise :nothing))\n~~~\n\nNote that there is also http://quickdocs.org/string-case/.\n\n### match (experimental) · new in Feb, 2024\n\nA COND-like macro to match substrings and bind variables to matches. Regular expressions are allowed for matches.\n\n`_` is a placeholder that is ignored.\n\nTHIS MACRO IS EXPERIMENTAL and might break in future releases.\n\nExample:\n\n```lisp\n(str:match \"a 1 b 2 d\"\n  ((\"a \" x \" b \" y \" d\") ;; =\u003e matched\n   (+ (parse-integer x) (parse-integer y)))\n  (t\n   'default-but-not-for-this-case)) ;; default branch\n;; =\u003e 3\n\n(str:match \"a 1 b c d\"\n  ((\"a 2 b\" _ \"d\") ;; =\u003e not matched\n   (print \"pass\"))\n  ((\"a \" _ \" b c d\") ;; =\u003e matched\n   \"here we go\")\n  (t 'default-but-not-for-this-case)) ;; default branch\n;; =\u003e \"here we go\"\n```\n\nMatch with regexs:\n\n```lisp\n(str:match \"123 hello 456\"\n ((\"\\\\d+\" s \"\\\\d+\")\n   s)\n (t \"nothing\"))\n;; =\u003e \" hello \"\n```\n\n## Changelog\n\n* September, 2025:\n  * optimized `repeat` further and fixed for LispWorks (no more using `apply` and its call argument limit), thanks to @yehouda.\n  * small backward uncompatible correctness changes, thanks to @swapneils:\n    * `join` now treats a `nil` separator as the empty string, rather than the string `\"nil\"`, conforming to the semantics in the README of not converting `nil` to `\"nil\"`.\n    * `containsp` now automatically returns `nil` if either string input is `nil`.\n    * `alphanump` and similar now use the correct regex checks for multi-line strings, rather than accepting an input if it has any line matching their specification.\n  * optimization changes, thanks to @swapneils:\n    * multiple non-recursive functions have been inlined.\n    * multiple functions have `ftype` declarations.\n* April, 2025:\n  * optimized `repeat` for large workloads. When the workload is\n    sufficiently large, `str:repeat` allocates all chunks of the\n    expected output onto the stack to call concat, causing an\n    overflow. For workloads larger than 10,000 characters in the\n    expected output, it now chunks the work to minimize the chance of\n    overflowing the stack or heap, while retaining decent performance.\n* Feb, 2024:\n  * added the `match` macro. It is EXPERIMENTAL and might change in future versions. We welcome your bug reports and feedback.\n* 0.21, November, 2023:\n  * added the `regex` key argument to `split`, `rsplit`, `split-omit-nulls`.\n* August, 2023:\n  * added the `regex` key argument to the `replace-*` functions.\n* March, 2023:\n  * added `str:ensure`, `str:ensure-prefix`, `str:ensure-suffix`, `str:ensure-wrapped-in` and `str:wrapped-in-p`.\n* January, 2023: added the `:char-barg` parameter to `trim`, `trim-left`, `trim-right`.\n  - minor: `ends-with-p` now works with a character.\n* June, 2022: small breaking change: fixed `prefixp` when used with a smaller prefix: \"f\" was not recognized as a prefix of \"foobar\" and \"foobuz\", only \"foo\" was. Now it is fixed. Same for `suffixp`.\n* Feb, 2022: added `fit`: fit the string to the given length: either shorten it, either padd padding.\n* 0.20, May, 2021: added `ascii-p`.\n* 0.19.1, May, 2021: speed up `join` (by a factor of 4).\n* 0.19, October, 2020: added s-member\n*0.18.1, September, 2020: fix replace-all edge case when the replacement string ends with two backslashes and a single quote.\n* 0.18, June, 2020: added `replace-using`.\n* 0.17, April 2020:\n  - added `collapse-whitespaces`\n  - `join` and `split` also accept a char as separator\n  - fixed `remove-punctuation` that did not respect the case. Use `no-case` for this\n  - fixed `from-file` \"odd number of arguments\" error.\n* 0.16, November 2019: added `pad`, `pad-[left, right, center]`.\n* 0.15, October 2019: added functions to change case (based on cl-change-case).\n  added remove-punctuation.\n* 0.14, October, 2019: fixed the cl-ppcre inconsistency in `split` and `lines`. A trailing separator now returns a trailing empty string.\n\nBefore:\n\n~~~lisp\n(str:split \" \" \"a b c \")\n(\"a\" \"b\" \"c\")  ;; like cl-ppcre:split\n~~~\n\nNow:\n\n~~~lisp\n(str:split \" \" \"a b c \")\n(\"a\" \"b\" \"c\" \"\")\n~~~\n\n* august, 2019: deprecated `prune`, renamed to `shorten`.\n* added `:limit` to `split`.\n* 0.13 june, 2019\n  - added `insert`\n* 0.12\n  - added case predicates (`downcasep`, `alphap`, `has-x` and friends).\n* 0.11 (Quicklisp end of march, 2019, also in Ultralisp)\n  - added `str:downcase`, `str:upcase` and `str:capitalize`, that fix the `nil` argument surprise.\n* 0.10\n  - `split` doesn't fix cl-ppcre's inconsistency anymore (when the separator appears at the end). See issue #18. So `(str:split \"xx\" \"fooxxbarxx\")` doesn't return a trailing `\"\"`.\n  - added `s-last`\n  - `s-first` and friends return `nil` when appropriate, not `\"\"`.\n* 0.9\n  - added `s-first` , `s-rest` and `s-nth`\n  - added `prefix` and `suffix` functions and predicates.\n  - added `prune`.\n* 0.8 added `string-case`\n* 0.7 added `version`\n* 0.6 added `split-omit-nulls` (QL, january 2018)\n* 0.5 added `common-prefix`\n* 0.4 added `from-file` and `to-file`.\n* 0.3 added `substring`.\n\n## Dev and test\n\nRegression testing is implemented with [fiveam](https://github.com/lispci/fiveam).\n\n### Main test suite\n\nEither use\n```lisp\n  (asdf:test-system :str)\n```\n\nor load the test package `str.test` and then\n```lisp\n  (fiveam:run! 'test-str:str)\n```\n\n### Specific test suite\n\n```lisp\n  (fiveam:run! 'test-str:replace-functions)\n```\n\nTest suite names:\n- replace-functions\n- lengthen-functions\n- ensure-functions\n- pad-functions\n- substring-functions\n- list-functions\n- from-list-to-string\n- from-list-to-list\n- from-string-to-list\n- predicates, case-functions\n- miscellaneous\n\n### Specific test\n\n```lisp\n  (fiveam:run! 'test-str::downcase) ;; (test symbols are unexported)\n```\n\n### Test when defined\n\nFirst you need to\n```lisp\n(setf fiveam:*run-test-when-defined* t)\n```\nthen the test is run after each definition / compilation.\nThis can be done with C-c C-c on emacs.\n\n## See also\n\n* the [Common Lisp Cookbook](https://lispcookbook.github.io/cl-cookbook/strings.html), strings page.\n* my [Common Lisp course on Udemy: from novice to effective developer](https://www.udemy.com/course/common-lisp-programming/?referralCode=2F3D698BBC4326F94358). Check out my blog for regular coupons.\n* https://lisp-journey.gitlab.io/\n* video: [how to create a Common Lisp project from scratch with our project generator](https://www.youtube.com/watch?v=XFc513MJjos): it sums up in 5 minutes what took me a much longer time to gather.\n\nInspired by the famous Emacs Lisp's [s.el](https://github.com/magnars/s.el).\n","funding_links":["https://github.com/sponsors/vindarel","https://ko-fi.com/vindarel"],"categories":["Common Lisp","Expert Systems"],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvindarel%2Fcl-str","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fvindarel%2Fcl-str","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fvindarel%2Fcl-str/lists"}