# s.el: the long lost Emacs string library, in one file

> s.el is a single file of Emacs Lisp that fills in the string functions Emacs never had, with a deliberately small vocabulary of s-prefixed names grouped by intent rather than by implementation. The naming is the design, and the one gap in the recent release history is worth understanding before you depend on it.

**magnars/s.el** — The long lost Emacs string manipulation library.

- Repository: https://github.com/magnars/s.el
- Stars: 1,075 · Forks: 98
- Language: Emacs Lisp
- License: GPL-3.0
- Published: 2026-10-19 · Updated: 2026-10-19 · Language: en
- Canonical page: https://hysenlabs.com/projects/magnars-s-el

## One file, one prefix, one naming convention

s.el is described in one line as the long lost Emacs string manipulation library, and the repository is almost as small as that description. The top level holds s.el itself, a LICENSE, a README, a Cask file, a set of scripts, a dev directory and a couple of configuration files.

That single-file shape is the point, not an accident of packaging. s.el can be dropped into a load path and it works, with no dependency to resolve and nothing to compile. The README says it is available on Melpa with M-x package-install s, or you can just dump s.el in your load path somewhere.

The naming convention is equally deliberate. Every function is prefixed s-, and the argument order is consistent: the string being operated on comes last in most of them, so the transformation reads left to right. s-pad-left takes len, padding and s, s-chop-suffix takes suffix and s, s-with takes s, form and more.

The result is a vocabulary rather than a grab bag. An Emacs Lisp programmer who learns s-trim and s-snake-case has learned the shape of the library, and the naming makes the code reviewable: if the function starts with s-, the reader knows what kind of operation it is and roughly where the string sits in the argument list.

The licence is GPL-3.0, with the LICENSE file in the repository, and the project is maintained by Magnar Sveen.

## The function list is grouped by intent, not by implementation

The README organises the API into groups, and the grouping is the fastest way to understand what is in the box.

Tweak whitespace covers s-trim, s-trim-left, s-trim-right, s-chomp, s-collapse-whitespace, s-word-wrap, s-center, s-pad-left and s-pad-right. To shorter string covers s-truncate, s-left, s-right, s-chop-left, s-chop-right, s-chop-suffix, s-chop-suffixes, s-chop-prefix, s-chop-prefixes, s-shared-start and s-shared-end. To longer string has s-repeat, s-concat, s-prepend, s-append and s-splice.

Then there is a group for conversion to and from lists, with s-lines, s-match, s-match-strings-all, s-matched-positions-all, s-slice-at, s-split, s-split-up-to and s-join. A predicates group holds s-equals?, s-less?, s-matches?, s-blank?, s-present?, s-ends-with?, s-starts-with?, s-contains?, s-lowercase?, s-uppercase?, s-mixedcase?, s-capitalized? and s-numeric?.

And there is a section the README calls the misc bucket, with s-replace, s-replace-all, s-downcase, s-upcase, s-capitalize, s-titleize, s-with, s-index-of, s-reverse, s-presence, s-format, s-lex-format, s-count-matches and s-wrap.

The last group is the one worth pausing on, because it is where a library stops being tidy. A function named s-format with a replacer argument is doing something specific, s-lex-format takes a format string, and s-with takes a format and more arguments, which is the shape of a templating helper rather than a string operator. The misc bucket is the receipt for the fact that a small library accretes.

The last group, pertaining to words, is the one most people adopt the library for: s-split-words, s-lower-camel-case, s-upper-camel-case, s-snake-case, s-dashed-words, s-capitalized-words, s-titleized-words, s-word-initials and s-blank-str?.

## Some functions do not do quite what the name suggests

The examples in the README are the specification, and a few of them describe behaviour that will surprise you if you only read the function name.

s-truncate is the clearest. If s is longer than len, cut it down to len - 3 and add ... at the end, so (s-truncate 6 "This is too long") gives "Thi..." and (s-truncate 16 "This is also too long") gives "This is also ...". The total length of the result is len, including the three dots, not len plus three.

s-center pads with spaces to centre the string, and s-pad-left and s-pad-right take an explicit padding string, so (s-pad-left 3 "0" "3") gives "003". A string already at or over the length is returned unchanged, which (s-center 1 "abc") shows as "abc".

s-word-wrap wraps words with newlines but does not break them, so a long hyphenated token stays on one line. s-chomp removes exactly one trailing newline, carriage return or carriage return plus newline, which means "some newlines\n\n" becomes "some newlines\n" rather than losing both.

And s-collapse-whitespace converts all adjacent whitespace characters to a single space, which includes newlines and tabs, not just spaces.

```elisp
(s-truncate 6 "This is too long") ;; => "Thi..."
(s-pad-left 3 "0" "23") ;; => "023"
(s-chomp "some newlines\n\n") ;; => "some newlines\n"
```

None of this is a defect. It is a library that was designed once, documented with examples, and then left alone, which is exactly the trade you make when you adopt a small library with a long history.

## Word case conversion, and the functions built on top of them

The word group is what pulls most people in, because converting between identifier styles is tedious and everybody writes it badly once.

s-split-words breaks a string into its words, and the four case converters build on that: s-lower-camel-case, s-upper-camel-case, s-snake-case and s-dashed-words. Alongside them sit s-capitalized-words, s-titleized-words, s-word-initials and s-blank-str?.

The related string functions live in the misc bucket, and they are the ones that make a naming convention usable. s-capitalize and s-titleize handle the capitalisation of a whole string, s-downcase and s-upcase handle case, and s-replace and s-replace-all handle substitution, with s-replace-all taking a list of replacements rather than a single old and new pair.

s-with is the one to look at first if you are building a formatter. Its signature is s, form and more, so it takes a string, a format and additional arguments, and s-format takes a template, a replacer and an optional extra argument. s-lex-format takes a format string in a different style, and s-wrap takes a string with a prefix and an optional suffix, which is a small thing that removes a lot of concatenation.

s-count-matches counts regex matches between optional start and end positions, and s-index-of finds a needle with an optional ignore-case flag. s-reverse reverses a string, and s-presence presumably answers whether a string is present in a sense the README would have to explain.

The combination of a case converter, a template function and a replace function is what makes this library useful in the place most Emacs users meet the problem, which is generating a name from a value.

## Splitting, joining and the regex helpers

The list conversion group is where s.el goes beyond what the built-in functions give you cleanly, and it is worth knowing the exact signatures because they carry useful options.

s-split takes a separator, a string and an optional omit-nulls flag. s-split-up-to takes the same plus a count, so you can split only the first n occurrences, which is the standard way to split a path on the first separator. s-join takes a separator and a list of strings. s-lines is the basic split on newlines.

The regex group is richer than the names suggest. s-match takes a regexp, a string and an optional start position. s-match-strings-all returns all matches for a regex against a string, and s-matched-positions-all returns the matched positions, with optional subexp-depth for the capture group. s-slice-at splits a string at a regexp, and s-count-matches counts matches with optional start and end bounds.

The two direction functions, s-shared-start and s-shared-end, take two strings and return the prefix or suffix they have in common, which is the sort of thing you need when comparing two paths or two generated identifiers.

s-matches? is the predicate counterpart of s-match, and it takes the same optional start position, so a predicate can be applied to the part of a string you care about without slicing it first. s-equals? and s-less? are the other comparison predicates, and s-blank? and s-present? are the two that show up most in configuration code, with s-blank-str? as the word-group variant of the same idea.

## Melpa, a single file, and a four year gap in the release history

The installation story is one command. From Melpa, it is M-x package-install followed by s, and the alternative is to put s.el somewhere in your load path.

The release history is where the interesting wrinkle is. There are two tags. 1.13.0, labelled Stable, is from 2022-08-29, and 1.13.1 is from 2026-08-01, and the last push to the repository was on 2026-08-02.

So the library sat at 1.13.0 for four years and then moved to 1.13.1, a patch bump after a very long gap. For a library whose entire surface is a list of small, well-understood functions, that is not a warning sign in itself; there is very little in s-trim that needs to change. But it does tell you how the project is run, and a reader should know that before they take a dependency on it.

The repository shows a conventional Elisp development setup around the single file. There is a Cask file for the development dependencies, a .travis.yml, a ci.sh, a run-tests.sh and a watch-tests.watchr file for running the tests, a pre-commit.sh, a dev directory, and a .github directory with the test workflow that the badge in the README refers to. There is also a coveralls badge for coverage and a create-docs.sh script next to a readme-template.md.

That last pair is the interesting one, because it suggests the README is assembled from a template by a script rather than written by hand, which is how the function list and the examples can stay in step with the code and its tests.

## Conclusion

s.el fits an Emacs Lisp developer who writes enough elisp to notice that the built-in string handling is awkward, and who wants one prefix and one naming convention instead of a dozen subtly different functions. It does not fit a project that wants a maintained general purpose string library with a larger surface, because this one is deliberately small and mostly unchanged since 2022. Before you adopt it, install it from Melpa and check the function list against what your code actually calls, since the gap between 1.13.0 in 2022 and 1.13.1 in 2026 is a fact about how this library is maintained rather than about how it works.

## FAQ

### How do I install s.el for Emacs?

It is available on Melpa, so you can run M-x package-install and then s, or you can just dump s.el in your load path somewhere. The library has no dependencies beyond Emacs itself.

### What is the difference between s-truncate and s-left?

s-truncate takes the first len characters of a string and, if the string is longer than len, cuts it to len - 3 and adds an ellipsis, so (s-truncate 6 "This is too long") gives "Thi...". s-left returns up to the len first characters without adding anything.

### Does s-word-wrap break long words?

No. The README example shows that s-word-wrap 10 on "It-wraps-words-but-does-not-break-them" returns the string unchanged, because it wraps words with newlines rather than breaking a word across lines.

### Which case conversion functions does s.el provide?

The word group includes s-split-words, s-lower-camel-case, s-upper-camel-case, s-snake-case, s-dashed-words, s-capitalized-words, s-titleized-words, s-word-initials and s-blank-str?, with s-capitalize, s-upcase and s-downcase in the misc bucket.

### How often is s.el updated?

The release history lists 1.13.0 Stable from 2022-08-29 and 1.13.1 from 2026-08-01, and the last push to the repository was on 2026-08-02. The library is small and largely unchanged between those two tags.

## Sources

- [Issues](https://github.com/magnars/s.el/issues)
- [License: GPL-3.0](https://github.com/magnars/s.el/blob/master/LICENSE)
- [magnars/s.el on GitHub](https://github.com/magnars/s.el)
- [README](https://github.com/magnars/s.el/blob/master/README.md)
- [Releases](https://github.com/magnars/s.el/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/magnars-s-el
