How Do I Add Line Numbers to End of Each Line? (useful to convert a table of contents to an alphabetical index)

Is it possible to create a regex to convert the following strings:

This is a tree
An olive in the glass
Peel the orange

To:

This is a tree 1
An olive in the glass 2
Peel the orange 3

All I do is add space integer (up to 3 digits) at the end of each sentence (defined as return or linefeed)

My problem is that it is not a search & replace because of the incremental digit at the end of each line.

Thanks very much!

One approach is to use very simple regex inside an Execute a JavaScript action.

A full language will give you a bit more flexibility, not only providing things like sorting, but also enabling you to:

  1. attach names to decomposed parts of the string, and then
  2. rejoin them in any pattern you like.

If, for example, you had split each line of a text like:

    1. apple
    2. orange 
    3. pear

into an index part and a phrase part, you could recombine them in a variety of patterns like:

`${phrase}\t${index}`

(Where the \t is a tab character).

This kind of pattern, for example:

Index from TOC.kmmacros (19.8 KB)

JS Source
(() => {
    'use strict';

    // main :: IO ()
    const main = () => {
        const
            tocString = Application('Keyboard Maestro Engine')
            .getvariable('tocString');

        return sortBy(
            comparing(x => x[0])
        )(
            tocString.split(/[\n\r]+/)
            .map(entry => {
                // Split on any white space, except at the end.
                const parts = entry.trim().split(/\s+/);

                // The phrase :: the remaining 
                // parts, delimited by single spaces.
                const phrase = parts.slice(1).join(' ');

                // The index :: first token, 
                // without any trailing dot.
                const index = parts[0].split('.')[0];

                return (`${phrase}\t${index}`);
            })
        )
        .join('\n');
    }

    // --------------------- GENERIC ---------------------

    // comparing :: (a -> b) -> (a -> a -> Ordering)
    const comparing = f =>
        x => y => {
            const
                a = f(x),
                b = f(y);
            return a < b ? -1 : (a > b ? 1 : 0);
        };

    // list :: StringOrArrayLike b => b -> [a]
    const list = xs =>
        // xs itself, if it is an Array,
        // or an Array derived from xs.
        Array.isArray(xs) ? (
            xs
        ) : Array.from(xs || []);


    // sortBy :: (a -> a -> Ordering) -> [a] -> [a]
    const sortBy = f =>
        xs => list(xs).slice()
        .sort((a, b) => f(a)(b));

    return main();
})();

@ronald, I moved your post to a new topic since it is a new question.

You don't really need RegEx for this.

Here is some simple pseudo code to outline the Actions:

Initialize a KM Variable "Local__LineNum" to 0
Use the KM For Each action with a lines collection in a KM Variable, I'll call "Local__SourceStr", and a loop variable "Local__Line".
In the For Each loop

  1. Increment a counter "Local__LineNum" by 1
  2. append a new Variable "Local__SourceWithLineNumbers" to
    %Local__Line% %Local__LineNum%

This is NOT everything you need, but should give you the general idea for a solution.

I don’t know where the text is when you start, but if it’s on the clipboard, a shell script of

pbpaste | perl -nle 'print "$_ $."' | pbcopy

will replace the unnumbered text with numbered text on the clipboard. If you expect to have a trailing linefeed at the end of the text and you don’t want that last blank “line” numbered, change the script to this:

pbpaste | perl -nle 'print "$_ $." unless (eof && !$_)' | pbcopy
2 Likes

Here’s another way to do it, which takes from the pasteboard and sends the output back to standard output:

pbpaste | rev | cat -n | rev

rev reverses the characters on each line.

cat -n adds line numbers.

rev again puts it back into the original order, with the line numbers now at the end.

UPDATE:

As noted below, this will only work if you have 1-9 lines. After 9 it will also reverse the line numbers so 10 will become ’01’.

1 Like

@ronald, this is SO EASY just using native, non-scripting, KM Actions, that I feel compelled to provide for all KM users, whom I think most of are more comfortable with non-scripting solutions.

Shell scripts can be very powerful, but also very dangerous if you don't know what you are doing, or how to understand and use scripts provided by others. So I always recommend a KM solution unless there is a compelling reason to use a script.

Example Output

image

Below is just an example written in response to your request. You will need to use as an example and/or change to meet your workflow automation needs.

Please let us know if it meets your needs.

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

MACRO:   Add Line Numbers to Text String [Example]

-~~~ VER: 1.0    2021-01-31 ~~~
Requires: KM 8.2.4+   macOS 10.11 (El Capitan)+
(Macro was written & tested using KM 9.0+ on macOS 10.14.5 (Mojave))

DOWNLOAD Macro File:

Add Line Numbers to Text String [Example].kmmacros
Note: This Macro was uploaded in a DISABLED state. You must enable before it can be triggered.


3 Likes

thank you

I will give it a try. thanks

pbpaste | rev | cat -n | rev

Celver, but wouldn't that end up reversing the numbers?

So you'd get

…
line 01
line 11
line 21
line 31
…
2 Likes

I read about pbpaste but it's still not clear as to what it is doing.
When you say pasteboard, do you mean clipboard ?
Let's assume that I start off by selecting the text
Do I have to copy the text to the clipboard before running the shell script ?. Will the output of the script automatically paste, or should I add a paste action ?

Thank you for creating a new topic.

Your macro works perfectly. Thanks very much!

The macro is very useful to convert a table of contents to an alphabetical index.

1 Like

Thanks.

You don't need to use a test for CLIPBOARDSEED() since the KM Copy Action has that built in.
Also, you can just do the For Each on the Clipboard.

Here's how I'd write your Macro:

image

1 Like

I added this a the end for alphabetical sorting. Works fine.

The Perl scripts from the forum did not work.

Since I was a bit harsh on shell scripts above, let me now say that, IMO, there are a few use cases where a simple, canned, shell script can be the best solution. I have this one saved in my KMFAM collection, and it seems to work fine here:

image

If any of you shell script gurus care to comment or jump in, please do.

I would expect, but have not tested, that a simple shell script like this would be faster can calling a BBEdit Text Factory, but I could be wrong.

1 Like

UGH. Yes. How dumb of me to miss that. So this will work great… as long as you have fewer than 10 lines.

1 Like

Yes. The two terms are used interchangeably. I believe Keyboard Maestro uses clipboard but Apple (at least for pbpaste and pbcopy uses ‘pasteboard’).

Yes. pbpaste translates to “take what is on the clipboard/pasteboard and send it to output” so whatever you want to work on must already be on it.

You would need to tell the Keyboard Maestro “Execute Shell Script” action to "paste results” (or save results to a variable, or whatever else is suitable).

Note that, as Peter rightly pointed out, my solution only works for fewer than 10 lines.

If you want a simple shell-based solution, instead of mine, you should use Dr Drang’s suggestion.

Note that his uses pbcopy at the end which means “Save the result to the clipboard/pasteboard” so you would not have to tell Keyboard Maestro to do that.

1 Like

Following up @tjluoma's comment, the reason I have pbpaste and pbcopy in my script is that I was sitting in front of an iPad when I wrote the answer and forgot that Keyboard Maestro has options for dealing with the clipboard as input and output. This would be the better way:

If you're not worried about the possibility of having a numbered blank line at the end, awk is shorter:

You could, of course, surround this step with Copy and Paste actions to get a macro that replaces the selected text with the line-numbered version of it.

2 Likes

thanks

thank you for the explanations

1 Like

Thank you very much @JMichaelTX

I have a question for @JMichaelTX @drdrang @tjluoma @ComplexPoint

@JMichaelTX brings up an interesting point. In fact he opens a pandora's box, which is to use the simplest fastest tool for the task. In this case it is a one word shell script inside a KBM action.

For a novice, shell scripts are cryptic even though I went so far as to read the take control book on the subject.

In the example below @JMichaelTX uses a one word text transform shell script where I was struggling trying to figure out a regex.

Just as a start, I was wondering where I could find a repertory of very short shell scripts used for the many possible commonly used transformations of selected text. Just a few examples are adding quotes, title care, remove new line feed, remove blank lines, delete tabs, etc etc etc

thanks very much !

1 Like