ican


Nameican JSON
Version 0.2.2 PyPI version JSON
download
home_page
Summaryican is a simple version bumper/build pipeline orchestrator
upload_time2023-09-22 07:54:21
maintainer
docs_urlNone
author
requires_python
licenseMIT
keywords api auth
VCS
bugtrack_url
requirements No requirements were recorded.
Travis-CI No Travis.
coveralls test coverage No coveralls.
            [![build, publish, and release](https://github.com/jthop/ican/actions/workflows/build_pub_release.yml/badge.svg)](https://github.com/jthop/ican/actions/workflows/build_pub_release.yml)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![PyPI version](https://badge.fury.io/py/ican.svg)](https://badge.fury.io/py/ican)
[![CodeFactor](https://www.codefactor.io/repository/github/jthop/ican/badge)](https://www.codefactor.io/repository/github/jthop/ican)
[![MIT license](https://img.shields.io/badge/License-MIT-blue.svg)](https://lbesson.mit-license.org/)
[![GitHub last commit](https://img.shields.io/github/last-commit/jthop/ican)](https://github.com/jthop/ican)
[![GitHub repo size](https://img.shields.io/github/repo-size/jthop/ican?style=flat)](https://github.com/jthop/ican)
[![GitHub language count](https://img.shields.io/github/languages/count/jthop/ican?style=flat)](https://github.com/jthop/ican)
[![GitHub top language](https://img.shields.io/github/languages/top/jthop/ican?style=flat)](https://python.org)
[![Whos your daddy](https://img.shields.io/badge/whos%20your%20daddy-2.0.7rc3-brightgreen.svg)](https://14.do/)
[![works badge](https://cdn.jsdelivr.net/gh/nikku/works-on-my-machine@v0.2.0/badge.svg)](https://github.com/nikku/works-on-my-machine)


# :wave: ican

Because anything you ask of it, the response is **always** `ican`.

## :man_office_worker: Motivation

There are plenty of version bumpers and build tools.  But ican is the only one that is designed for the smallest teams, even the 1 man team.

The one man team has different procedures than most.  Often times the one man team forgets to bump version numbers.  Few even bother to keep a code repository besides their own "Dropbox."

ican brings big team development practices to small teams.  ican's build number is one feature.  As long as you use ican to automate your docker image builds, deployments, etc. each time your build number is incremented.  That way you always have a different semantic version number, even if you don't purposely bump one of the primary 3 parts.

You can even easily use ican to start tagging docker images, as well as git commits.  Soon your small team will be officially "tagging" and "releasing" software like you have a Fortune 500 CI/CD manager.

## :floppy_disk: Install

Install the ican package via pypi

```shell
pip install ican
```

## :toolbox: Sample Config

Config is done via the .ican file in your project's root diarectory.

Sample .ican config file

```ini
[version]
current = 0.1.6+build.40
previous = 0.1.5+build.39

[options]
log_file = ican.log

[file: version]
file = ./src/__init__.py
style = semantic
variable = __version__

[file: json_example]
file = ./package.json
style = public
variable = version
file_type = json

[pipeline: release]
step1 = ./clean_my_project.sh
step2 = $ICAN(bump {arg_1})
step2 = git commit -a
step3 = git tag -a {tag} --sign
step4 = git push origin master {tag}

```

### Explanation

- This config defines the current version as `0.1.6` with build # 40.
- All operations will be logged to the `ican.log` file.
- ican will update a variable named `__version__` in `./src/__init__.py` any time the bump command is run.
- ican will use the `semantic` style of the version when updating this file.
- A release pipeline has been defined.  More on that later.

### :exclamation: Important
Take note, all sections must be unique.  So if you define more than one [file: [LABEL]] section, make sure each one has a unique label.

### :thumbsdown: :exploding_head:
```ini
[file: py_code]
file = ./src/__init__.py
...
[file: py_code]
file = ./src/__main__.py
```

### :thumbsup: :sunglasses:
```ini
[file: src_init]
file = ./src/__init__.py
...
[file: main]
file = ./src/__main__.py
```

## :triangular_ruler: Config

| Section           | Key             | Value                                           |
| ----------------- | ----------------|-------------------------------------------------|
| version           | current         | This is the value that ican stores the current version number in. |
| version           | previous        | This is the previous version ican uses in case of rollback.       |
| options           | log_file        | All operations are logged to disk in this file.  To turn logging off, do not define the log_file. |
| file: [LABEL]     | file            | The filename of a file ican will update with new versions.  You can use a standard unix glob (*.py) if desired. |
| file: [LABEL]     | file_type       | The type of file.  Can be "python", "json", or "unknown".  Python/Unknown just search via regex.  Json will use the variable name as a key.  This field is optional as ican will use file extensions to guess the file type.
| file: [LABEL]     | style           | The version format to use.  Choices are [semantic, public, pep440, git] |
| file: [LABEL]     | variable        | The variable name pointing to the version string that ican will update when versions are bumped. |
| file: [LABEL]     | regex           | User-supplied python formattted regex string defining how to replace the file's version. |
| pipeline: [LABEL] | [STEP]          | A pipeline step is a cli command such as `git commit -a`.  **STEP values MUST to be unique.**  |


### :mag: User-supplied regex

When searching for a variable, ican will search for the variable's name, followed by an `=` symbol, followed by a value in either single or double quotes.  There can be spaces or no spaces on either side of the `=` symbol.  This covers most use cases.

If your use case is more complicated, you can omit the `variable` line in your config file and instead include a `regex` value instead.  This should be a pyton formatted regex string with a named group to identify the `version` ican will replace.


```ini
[file1]
file = ./src/__init__.py
style = semantic
regex = __version__\s*=\s*(?P<quote>[\'\"])(?P<version>.+)(?P=quote)
```

### :computer: Pipelines

#### Pipeline Intro
Pipelines allow you to define cli commands as well as internal ican functions to be run in a batch.  They are defined inside your .ican file.

Example:

```ini
[pipeline: release]
step1 = $ICAN(bump {arg_1})
step2 = git add .
step3 = git commit -m "auto-commit for {tag}"
step4 = git tag -a {tag} -m "automated tag for release {tag}" --sign
step5 = git push origin master
step6 = $ICAN(show)
```

You could explicitly tell ican to run the `release` pipeline with the following command:

* `ican run release patch`

* Step1 uses the variable `arg_1`.  *{arg_1}* references the *first* user supplied argument.
 
* In the example above, `patch` is the value assigned to {arg_1}

* ican allows you to leave out `run` and simply type: `ican release patch`

* Finally, if you do not supply an argument, ican will use the function's default.  The bump default is `build`.  The following command will bump build instead of patch before commiting to git.
  - `ican release`


### :exclamation: Important
Each [pipeline: LABEL] needs a unique LABEL value.  Pipeline steps in the same pipeline must be unique as well.  

### :thumbsdown: :exploding_head:
```ini
[pipeline: hello]
step1 = echo 'hello world'
...
[pipeline: hello]
step1 = echo 'this is another pipeline'
step1 = echo 'this example HAS 2 ISSUES'
step1 = echo 'can you spot them?'
```

### :thumbsup: :sunglasses:
```ini
[pipeline: hello]
step1 = echo 'hello world'
...
[pipeline: another]
step1 = echo 'this is another pipeline with a unique label'
step2 = echo 'this example has unique step names within each pipeline'
step3 = Batter.home_run()
```

#### Pipeline Context
The pipeline context is available in 2 locations

* You can template your pipeline steps, using the {variable_name} format. Example: `git push origin master {tag}`
* Before a pipeline runs, ican will inject your shell's environment with all pipeline context variables prefixed with ICAN_.

For example you can access `semantic` in your ENV as `ICAN_SEMANTIC`

#### Pipeline Context Variables

| variable      | Description                                      |
| --------------|--------------------------------------------------|
| semantic      | the current version in semantic format           |
| public        | the current version in public format             |
| pep440        | the current version canonical with pep440        |
| git           | the current version using git metadata           |
| major         | the major portion of the semantic version        |
| minor         | the minor portion of the semantic version        |
| patch         | the patch portion of the semantic version        |
| prerelease    | the major portion of the semantic version        |
| build         | the build number                                 |
| tag           | the git tag, `v{public_version}`                 |
| age           | REBUILD if bump build, NEW all other bumps       |
| env           | DEVELOPMENT or PRODUCTION based on the version   |
| root          | the root directory of your project               |
| previous      | the previous semantic version                    |

## :muscle: Use

You can use ican via the CLI in a typical fashion, using the format below

```shell
ican [command] [arguments] [options]
```

## :dog2: Commands

| Command    | Arguments               | Options        | Description   |
| -----------| ------------------------| -------------  | ------------- |
| bump       | **PART** `required`     |                | Increments the **PART** of the semantic version.  <br /> [*major*, *minor*, *patch*, *prerelease*] |
| bump       |                         | --pre `TOKEN`  | If bumping prerelease, set the **TOKEN** to [*alpha*, *beta*, *rc*, *dev*] |
| pre        | **TOKEN** `required`    |                | Set the prerelease **TOKEN** without bumping.
| show       | **STYLE** `required`    |                | Shows the current version with the format **STYLE**. <br /> [*semantic*, *public*, *pep440*, *git*] |
| run        | **PIPELINE** `required` |                | Run the specified **PIPELINE**  |
| rollback   | none                    |                | Rollback to the previously persisted version.  |
| init       | none                    |                | Initialize your project with default config in the current directory.   |


## :roll_eyes: Options

The output and parsing of `ican` can be controlled with the following options.

| Name                   | Description                                                  |
| -------------          | -------------                                                |
| `--verbose`            | To aid in your debugging, verbose prints all messages.       |
| `--dry-run`            | Useful if used WITH --verbose, will not modify any files.    |
| `--version`            | This will displpay the current version of ican.

## :eyes: Examples

```bash
$ ican init

...

$ ican show current
0.2.7-beta.3+build.99

# Bump with no arguments defaults to bump the build number.
$ ican bump
0.2.7-beta.3+build.100

# Now its release time.  Lets bump the minor
$ ican bump minor
0.3.0+build.101

# Oh no, major problem, rollback
$ ican rollback
0.3.0+build.100

# Use an aliaw
$ ican deploy
+BEGIN pipeline.NEW.RELEASE
git commit successful
+END pipeline.NEW.RELEASE
1.0.0+build.101

# now run our docker pipeline
$ ican run docker
+BEGIN pipeline.DOCKER
docker container build...
+END pipeline.DOCKER
1.0.0+build.101
```

            

Raw data

            {
    "_id": null,
    "home_page": "",
    "name": "ican",
    "maintainer": "",
    "docs_url": null,
    "requires_python": "",
    "maintainer_email": "",
    "keywords": "api,auth",
    "author": "",
    "author_email": "",
    "download_url": "https://files.pythonhosted.org/packages/8f/fe/916cbf91328b9d44746f72a4d29cc6af69ffd6ed812003f33b5039107069/ican-0.2.2.tar.gz",
    "platform": null,
    "description": "[![build, publish, and release](https://github.com/jthop/ican/actions/workflows/build_pub_release.yml/badge.svg)](https://github.com/jthop/ican/actions/workflows/build_pub_release.yml)\n[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)\n[![PyPI version](https://badge.fury.io/py/ican.svg)](https://badge.fury.io/py/ican)\n[![CodeFactor](https://www.codefactor.io/repository/github/jthop/ican/badge)](https://www.codefactor.io/repository/github/jthop/ican)\n[![MIT license](https://img.shields.io/badge/License-MIT-blue.svg)](https://lbesson.mit-license.org/)\n[![GitHub last commit](https://img.shields.io/github/last-commit/jthop/ican)](https://github.com/jthop/ican)\n[![GitHub repo size](https://img.shields.io/github/repo-size/jthop/ican?style=flat)](https://github.com/jthop/ican)\n[![GitHub language count](https://img.shields.io/github/languages/count/jthop/ican?style=flat)](https://github.com/jthop/ican)\n[![GitHub top language](https://img.shields.io/github/languages/top/jthop/ican?style=flat)](https://python.org)\n[![Whos your daddy](https://img.shields.io/badge/whos%20your%20daddy-2.0.7rc3-brightgreen.svg)](https://14.do/)\n[![works badge](https://cdn.jsdelivr.net/gh/nikku/works-on-my-machine@v0.2.0/badge.svg)](https://github.com/nikku/works-on-my-machine)\n\n\n# :wave: ican\n\nBecause anything you ask of it, the response is **always** `ican`.\n\n## :man_office_worker: Motivation\n\nThere are plenty of version bumpers and build tools.  But ican is the only one that is designed for the smallest teams, even the 1 man team.\n\nThe one man team has different procedures than most.  Often times the one man team forgets to bump version numbers.  Few even bother to keep a code repository besides their own \"Dropbox.\"\n\nican brings big team development practices to small teams.  ican's build number is one feature.  As long as you use ican to automate your docker image builds, deployments, etc. each time your build number is incremented.  That way you always have a different semantic version number, even if you don't purposely bump one of the primary 3 parts.\n\nYou can even easily use ican to start tagging docker images, as well as git commits.  Soon your small team will be officially \"tagging\" and \"releasing\" software like you have a Fortune 500 CI/CD manager.\n\n## :floppy_disk: Install\n\nInstall the ican package via pypi\n\n```shell\npip install ican\n```\n\n## :toolbox: Sample Config\n\nConfig is done via the .ican file in your project's root diarectory.\n\nSample .ican config file\n\n```ini\n[version]\ncurrent = 0.1.6+build.40\nprevious = 0.1.5+build.39\n\n[options]\nlog_file = ican.log\n\n[file: version]\nfile = ./src/__init__.py\nstyle = semantic\nvariable = __version__\n\n[file: json_example]\nfile = ./package.json\nstyle = public\nvariable = version\nfile_type = json\n\n[pipeline: release]\nstep1 = ./clean_my_project.sh\nstep2 = $ICAN(bump {arg_1})\nstep2 = git commit -a\nstep3 = git tag -a {tag} --sign\nstep4 = git push origin master {tag}\n\n```\n\n### Explanation\n\n- This config defines the current version as `0.1.6` with build # 40.\n- All operations will be logged to the `ican.log` file.\n- ican will update a variable named `__version__` in `./src/__init__.py` any time the bump command is run.\n- ican will use the `semantic` style of the version when updating this file.\n- A release pipeline has been defined.  More on that later.\n\n### :exclamation: Important\nTake note, all sections must be unique.  So if you define more than one [file: [LABEL]] section, make sure each one has a unique label.\n\n### :thumbsdown: :exploding_head:\n```ini\n[file: py_code]\nfile = ./src/__init__.py\n...\n[file: py_code]\nfile = ./src/__main__.py\n```\n\n### :thumbsup: :sunglasses:\n```ini\n[file: src_init]\nfile = ./src/__init__.py\n...\n[file: main]\nfile = ./src/__main__.py\n```\n\n## :triangular_ruler: Config\n\n| Section           | Key             | Value                                           |\n| ----------------- | ----------------|-------------------------------------------------|\n| version           | current         | This is the value that ican stores the current version number in. |\n| version           | previous        | This is the previous version ican uses in case of rollback.       |\n| options           | log_file        | All operations are logged to disk in this file.  To turn logging off, do not define the log_file. |\n| file: [LABEL]     | file            | The filename of a file ican will update with new versions.  You can use a standard unix glob (*.py) if desired. |\n| file: [LABEL]     | file_type       | The type of file.  Can be \"python\", \"json\", or \"unknown\".  Python/Unknown just search via regex.  Json will use the variable name as a key.  This field is optional as ican will use file extensions to guess the file type.\n| file: [LABEL]     | style           | The version format to use.  Choices are [semantic, public, pep440, git] |\n| file: [LABEL]     | variable        | The variable name pointing to the version string that ican will update when versions are bumped. |\n| file: [LABEL]     | regex           | User-supplied python formattted regex string defining how to replace the file's version. |\n| pipeline: [LABEL] | [STEP]          | A pipeline step is a cli command such as `git commit -a`.  **STEP values MUST to be unique.**  |\n\n\n### :mag: User-supplied regex\n\nWhen searching for a variable, ican will search for the variable's name, followed by an `=` symbol, followed by a value in either single or double quotes.  There can be spaces or no spaces on either side of the `=` symbol.  This covers most use cases.\n\nIf your use case is more complicated, you can omit the `variable` line in your config file and instead include a `regex` value instead.  This should be a pyton formatted regex string with a named group to identify the `version` ican will replace.\n\n\n```ini\n[file1]\nfile = ./src/__init__.py\nstyle = semantic\nregex = __version__\\s*=\\s*(?P<quote>[\\'\\\"])(?P<version>.+)(?P=quote)\n```\n\n### :computer: Pipelines\n\n#### Pipeline Intro\nPipelines allow you to define cli commands as well as internal ican functions to be run in a batch.  They are defined inside your .ican file.\n\nExample:\n\n```ini\n[pipeline: release]\nstep1 = $ICAN(bump {arg_1})\nstep2 = git add .\nstep3 = git commit -m \"auto-commit for {tag}\"\nstep4 = git tag -a {tag} -m \"automated tag for release {tag}\" --sign\nstep5 = git push origin master\nstep6 = $ICAN(show)\n```\n\nYou could explicitly tell ican to run the `release` pipeline with the following command:\n\n* `ican run release patch`\n\n* Step1 uses the variable `arg_1`.  *{arg_1}* references the *first* user supplied argument.\n \n* In the example above, `patch` is the value assigned to {arg_1}\n\n* ican allows you to leave out `run` and simply type: `ican release patch`\n\n* Finally, if you do not supply an argument, ican will use the function's default.  The bump default is `build`.  The following command will bump build instead of patch before commiting to git.\n  - `ican release`\n\n\n### :exclamation: Important\nEach [pipeline: LABEL] needs a unique LABEL value.  Pipeline steps in the same pipeline must be unique as well.  \n\n### :thumbsdown: :exploding_head:\n```ini\n[pipeline: hello]\nstep1 = echo 'hello world'\n...\n[pipeline: hello]\nstep1 = echo 'this is another pipeline'\nstep1 = echo 'this example HAS 2 ISSUES'\nstep1 = echo 'can you spot them?'\n```\n\n### :thumbsup: :sunglasses:\n```ini\n[pipeline: hello]\nstep1 = echo 'hello world'\n...\n[pipeline: another]\nstep1 = echo 'this is another pipeline with a unique label'\nstep2 = echo 'this example has unique step names within each pipeline'\nstep3 = Batter.home_run()\n```\n\n#### Pipeline Context\nThe pipeline context is available in 2 locations\n\n* You can template your pipeline steps, using the {variable_name} format. Example: `git push origin master {tag}`\n* Before a pipeline runs, ican will inject your shell's environment with all pipeline context variables prefixed with ICAN_.\n\nFor example you can access `semantic` in your ENV as `ICAN_SEMANTIC`\n\n#### Pipeline Context Variables\n\n| variable      | Description                                      |\n| --------------|--------------------------------------------------|\n| semantic      | the current version in semantic format           |\n| public        | the current version in public format             |\n| pep440        | the current version canonical with pep440        |\n| git           | the current version using git metadata           |\n| major         | the major portion of the semantic version        |\n| minor         | the minor portion of the semantic version        |\n| patch         | the patch portion of the semantic version        |\n| prerelease    | the major portion of the semantic version        |\n| build         | the build number                                 |\n| tag           | the git tag, `v{public_version}`                 |\n| age           | REBUILD if bump build, NEW all other bumps       |\n| env           | DEVELOPMENT or PRODUCTION based on the version   |\n| root          | the root directory of your project               |\n| previous      | the previous semantic version                    |\n\n## :muscle: Use\n\nYou can use ican via the CLI in a typical fashion, using the format below\n\n```shell\nican [command] [arguments] [options]\n```\n\n## :dog2: Commands\n\n| Command    | Arguments               | Options        | Description   |\n| -----------| ------------------------| -------------  | ------------- |\n| bump       | **PART** `required`     |                | Increments the **PART** of the semantic version.  <br /> [*major*, *minor*, *patch*, *prerelease*] |\n| bump       |                         | --pre `TOKEN`  | If bumping prerelease, set the **TOKEN** to [*alpha*, *beta*, *rc*, *dev*] |\n| pre        | **TOKEN** `required`    |                | Set the prerelease **TOKEN** without bumping.\n| show       | **STYLE** `required`    |                | Shows the current version with the format **STYLE**. <br /> [*semantic*, *public*, *pep440*, *git*] |\n| run        | **PIPELINE** `required` |                | Run the specified **PIPELINE**  |\n| rollback   | none                    |                | Rollback to the previously persisted version.  |\n| init       | none                    |                | Initialize your project with default config in the current directory.   |\n\n\n## :roll_eyes: Options\n\nThe output and parsing of `ican` can be controlled with the following options.\n\n| Name                   | Description                                                  |\n| -------------          | -------------                                                |\n| `--verbose`            | To aid in your debugging, verbose prints all messages.       |\n| `--dry-run`            | Useful if used WITH --verbose, will not modify any files.    |\n| `--version`            | This will displpay the current version of ican.\n\n## :eyes: Examples\n\n```bash\n$ ican init\n\n...\n\n$ ican show current\n0.2.7-beta.3+build.99\n\n# Bump with no arguments defaults to bump the build number.\n$ ican bump\n0.2.7-beta.3+build.100\n\n# Now its release time.  Lets bump the minor\n$ ican bump minor\n0.3.0+build.101\n\n# Oh no, major problem, rollback\n$ ican rollback\n0.3.0+build.100\n\n# Use an aliaw\n$ ican deploy\n+BEGIN pipeline.NEW.RELEASE\ngit commit successful\n+END pipeline.NEW.RELEASE\n1.0.0+build.101\n\n# now run our docker pipeline\n$ ican run docker\n+BEGIN pipeline.DOCKER\ndocker container build...\n+END pipeline.DOCKER\n1.0.0+build.101\n```\n",
    "bugtrack_url": null,
    "license": "MIT",
    "summary": "ican is a simple version bumper/build pipeline orchestrator",
    "version": "0.2.2",
    "project_urls": {
        "Bug Tracker": "https://github.com/jthop/ican/issues",
        "Homepage": "https://github.com/jthop/ican"
    },
    "split_keywords": [
        "api",
        "auth"
    ],
    "urls": [
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "32af625d2652bd9c15c9bf186e8bb2f59be1fcc778434d0f843f5fb32d02a95c",
                "md5": "766ff245f68428ccb19fded949c0ee61",
                "sha256": "da15ccb6b960e9f3c29e5fdc2057362934f3435e43c3d1446a12161c02fcfb13"
            },
            "downloads": -1,
            "filename": "ican-0.2.2-py3-none-any.whl",
            "has_sig": false,
            "md5_digest": "766ff245f68428ccb19fded949c0ee61",
            "packagetype": "bdist_wheel",
            "python_version": "py3",
            "requires_python": null,
            "size": 40005,
            "upload_time": "2023-09-22T07:54:19",
            "upload_time_iso_8601": "2023-09-22T07:54:19.940234Z",
            "url": "https://files.pythonhosted.org/packages/32/af/625d2652bd9c15c9bf186e8bb2f59be1fcc778434d0f843f5fb32d02a95c/ican-0.2.2-py3-none-any.whl",
            "yanked": false,
            "yanked_reason": null
        },
        {
            "comment_text": "",
            "digests": {
                "blake2b_256": "8ffe916cbf91328b9d44746f72a4d29cc6af69ffd6ed812003f33b5039107069",
                "md5": "4e2edb7e3aa9f981c1f341b562c6ea23",
                "sha256": "3db0c885565cb5bfd626ac95895a0d09b96390f3ffde8321b7dabb54ad5b2885"
            },
            "downloads": -1,
            "filename": "ican-0.2.2.tar.gz",
            "has_sig": false,
            "md5_digest": "4e2edb7e3aa9f981c1f341b562c6ea23",
            "packagetype": "sdist",
            "python_version": "source",
            "requires_python": null,
            "size": 41370,
            "upload_time": "2023-09-22T07:54:21",
            "upload_time_iso_8601": "2023-09-22T07:54:21.777785Z",
            "url": "https://files.pythonhosted.org/packages/8f/fe/916cbf91328b9d44746f72a4d29cc6af69ffd6ed812003f33b5039107069/ican-0.2.2.tar.gz",
            "yanked": false,
            "yanked_reason": null
        }
    ],
    "upload_time": "2023-09-22 07:54:21",
    "github": true,
    "gitlab": false,
    "bitbucket": false,
    "codeberg": false,
    "github_user": "jthop",
    "github_project": "ican",
    "travis_ci": false,
    "coveralls": false,
    "github_actions": true,
    "lcname": "ican"
}
        
Elapsed time: 0.11768s