linerepeat 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- linerepeat/__init__.py +4 -0
- linerepeat/cli.py +84 -0
- linerepeat/repeat.py +70 -0
- linerepeat/stream.py +87 -0
- linerepeat-0.1.0.dist-info/METADATA +98 -0
- linerepeat-0.1.0.dist-info/RECORD +9 -0
- linerepeat-0.1.0.dist-info/WHEEL +4 -0
- linerepeat-0.1.0.dist-info/entry_points.txt +3 -0
- linerepeat-0.1.0.dist-info/licenses/LICENSE +7 -0
linerepeat/__init__.py
ADDED
linerepeat/cli.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
|
|
4
|
+
import typer
|
|
5
|
+
|
|
6
|
+
from linerepeat.repeat import *
|
|
7
|
+
from linerepeat.stream import *
|
|
8
|
+
|
|
9
|
+
app = typer.Typer(add_completion=False)
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@app.command()
|
|
13
|
+
def linerepeat(
|
|
14
|
+
count: Annotated[
|
|
15
|
+
int,
|
|
16
|
+
typer.Argument(
|
|
17
|
+
min=1,
|
|
18
|
+
help="How many times to repeat.",
|
|
19
|
+
),
|
|
20
|
+
],
|
|
21
|
+
input_file: Annotated[
|
|
22
|
+
Path | None,
|
|
23
|
+
typer.Option(
|
|
24
|
+
"-i",
|
|
25
|
+
"--input",
|
|
26
|
+
exists=True,
|
|
27
|
+
dir_okay=False,
|
|
28
|
+
readable=True,
|
|
29
|
+
writable=True,
|
|
30
|
+
help="Edit this file in place instead of reading stdin.",
|
|
31
|
+
),
|
|
32
|
+
] = None,
|
|
33
|
+
output_file: Annotated[
|
|
34
|
+
Path | None,
|
|
35
|
+
typer.Option(
|
|
36
|
+
"-o",
|
|
37
|
+
"--output",
|
|
38
|
+
dir_okay=False,
|
|
39
|
+
help="Write here instead of stdout. Can't be used with --input.",
|
|
40
|
+
),
|
|
41
|
+
] = None,
|
|
42
|
+
separator: Annotated[
|
|
43
|
+
str,
|
|
44
|
+
typer.Option(
|
|
45
|
+
"-s",
|
|
46
|
+
"--separator",
|
|
47
|
+
help="Line placed between repeats.",
|
|
48
|
+
),
|
|
49
|
+
] = "",
|
|
50
|
+
mode: Annotated[
|
|
51
|
+
Mode,
|
|
52
|
+
typer.Option(
|
|
53
|
+
"-m",
|
|
54
|
+
"--mode",
|
|
55
|
+
help="Repeat single lines or blocks.",
|
|
56
|
+
),
|
|
57
|
+
] = Mode.LINE,
|
|
58
|
+
till: Annotated[
|
|
59
|
+
int,
|
|
60
|
+
typer.Option(
|
|
61
|
+
"-t",
|
|
62
|
+
"--till",
|
|
63
|
+
help="Lines per block (block mode).",
|
|
64
|
+
),
|
|
65
|
+
] = 0,
|
|
66
|
+
):
|
|
67
|
+
|
|
68
|
+
if input_file is not None and output_file is not None:
|
|
69
|
+
raise typer.BadParameter(
|
|
70
|
+
"can't be used with --input, which edits the file in place.",
|
|
71
|
+
param_hint="--output",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
# Open input (stdin or file) and process with repeat()
|
|
75
|
+
with open_input(input_file) as src:
|
|
76
|
+
try:
|
|
77
|
+
result = repeat(src, count, separator=separator, mode=mode, till=till)
|
|
78
|
+
except ValueError as e:
|
|
79
|
+
raise typer.BadParameter(str(e)) from e
|
|
80
|
+
|
|
81
|
+
# Write output to stdout, file, or back to input file
|
|
82
|
+
with open_output(input_path=input_file, output_path=output_file) as out:
|
|
83
|
+
write_lines(out, result)
|
|
84
|
+
|
linerepeat/repeat.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
from collections.abc import Iterable, Iterator
|
|
2
|
+
from enum import StrEnum
|
|
3
|
+
from itertools import islice
|
|
4
|
+
from itertools import repeat as _times
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Mode(StrEnum):
|
|
8
|
+
LINE = "line"
|
|
9
|
+
BLOCK = "block"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _clean(lines: Iterable[str]) -> Iterator[str]:
|
|
13
|
+
"""Strip trailing newlines and skip empty lines."""
|
|
14
|
+
for line in lines:
|
|
15
|
+
s = line.rstrip("\n\r")
|
|
16
|
+
if s.strip():
|
|
17
|
+
yield s
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _chunks(items: Iterator[str], size: int) -> Iterator[list[str]]:
|
|
21
|
+
"""
|
|
22
|
+
Group items into chunks of the given size.
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
items: Iterator of items to chunk.
|
|
26
|
+
size: Number of items per chunk.
|
|
27
|
+
|
|
28
|
+
Yields:
|
|
29
|
+
Lists of items, each up to 'size' long.
|
|
30
|
+
"""
|
|
31
|
+
while chunk := list(islice(items, size)):
|
|
32
|
+
yield chunk
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _generate(lines, count, separator, mode, till) -> Iterator[str]:
|
|
36
|
+
"""
|
|
37
|
+
In LINE mode: each line is repeated 'count' times, with separator between.
|
|
38
|
+
In BLOCK mode: lines are grouped into blocks of 'till' lines, then each
|
|
39
|
+
block is repeated 'count' times with separator between blocks.
|
|
40
|
+
"""
|
|
41
|
+
first = True
|
|
42
|
+
if mode == Mode.LINE:
|
|
43
|
+
for line in lines:
|
|
44
|
+
if not first:
|
|
45
|
+
yield separator
|
|
46
|
+
first = False
|
|
47
|
+
yield from _times(line, count)
|
|
48
|
+
else:
|
|
49
|
+
for block in _chunks(lines, till):
|
|
50
|
+
for _ in range(count):
|
|
51
|
+
if not first:
|
|
52
|
+
yield separator
|
|
53
|
+
first = False
|
|
54
|
+
yield from block
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def repeat(
|
|
58
|
+
lines: Iterable[str],
|
|
59
|
+
count: int,
|
|
60
|
+
*,
|
|
61
|
+
separator: str = "",
|
|
62
|
+
mode: Mode = Mode.LINE,
|
|
63
|
+
till: int = 0,
|
|
64
|
+
) -> Iterator[str]:
|
|
65
|
+
|
|
66
|
+
if count < 1:
|
|
67
|
+
raise ValueError("count must be at least 1.")
|
|
68
|
+
if mode == Mode.BLOCK and till < 2:
|
|
69
|
+
raise ValueError("till must be at least 2 in block mode.")
|
|
70
|
+
return _generate(_clean(lines), count, separator, mode, till)
|
linerepeat/stream.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import sys
|
|
4
|
+
import tempfile
|
|
5
|
+
from collections.abc import Generator, Iterable
|
|
6
|
+
from contextlib import contextmanager
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import TextIO
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@contextmanager
|
|
12
|
+
def open_input(path: Path | None) -> Generator[TextIO]:
|
|
13
|
+
"""Open an input file or stdin."""
|
|
14
|
+
if path is None:
|
|
15
|
+
yield sys.stdin
|
|
16
|
+
else:
|
|
17
|
+
with path.open(encoding="utf-8") as f:
|
|
18
|
+
yield f
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@contextmanager
|
|
22
|
+
def open_output(
|
|
23
|
+
*,
|
|
24
|
+
input_path: Path | None,
|
|
25
|
+
output_path: Path | None,
|
|
26
|
+
) -> Generator[TextIO]:
|
|
27
|
+
"""
|
|
28
|
+
Open an output destination based on arguments.
|
|
29
|
+
|
|
30
|
+
Priority:
|
|
31
|
+
1. If input_path is given: edit that file in place.
|
|
32
|
+
2. Else if output_path is given: write to that file.
|
|
33
|
+
3. Else: write to stdout.
|
|
34
|
+
|
|
35
|
+
Yields:
|
|
36
|
+
Open file handle for writing.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
if input_path is not None:
|
|
40
|
+
with _replace_in_place(input_path) as f:
|
|
41
|
+
yield f
|
|
42
|
+
elif output_path is not None:
|
|
43
|
+
with output_path.open("w", encoding="utf-8") as f:
|
|
44
|
+
yield f
|
|
45
|
+
else:
|
|
46
|
+
yield sys.stdout
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@contextmanager
|
|
50
|
+
def _replace_in_place(path: Path) -> Generator[TextIO]:
|
|
51
|
+
"""
|
|
52
|
+
Replace a file by writing to a temp file first then swap it.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
path: File to replace.
|
|
56
|
+
|
|
57
|
+
Yields:
|
|
58
|
+
Open temp file handle for writing.
|
|
59
|
+
"""
|
|
60
|
+
target = path.resolve()
|
|
61
|
+
fd, tmp_name = tempfile.mkstemp(
|
|
62
|
+
dir=target.parent,
|
|
63
|
+
prefix=f".{target.name}.",
|
|
64
|
+
suffix=".tmp",
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
tmp = Path(tmp_name)
|
|
68
|
+
try:
|
|
69
|
+
with os.fdopen(fd, "w", encoding="utf-8") as f:
|
|
70
|
+
yield f
|
|
71
|
+
shutil.copymode(target, tmp)
|
|
72
|
+
os.replace(tmp, target)
|
|
73
|
+
except BaseException:
|
|
74
|
+
tmp.unlink(missing_ok=True) # Clean up temp file on any error
|
|
75
|
+
raise
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def write_lines(out: TextIO, lines: Iterable[str]) -> None:
|
|
79
|
+
"""
|
|
80
|
+
Write lines to an output stream, adding newline after each.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
out: Output file handle.
|
|
84
|
+
lines: Lines to write.
|
|
85
|
+
"""
|
|
86
|
+
out.writelines(f"{line}\n" for line in lines)
|
|
87
|
+
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: linerepeat
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A small CLI tool for repeating lines
|
|
5
|
+
Keywords: cli,text,lines,repeat,typer
|
|
6
|
+
Author: Abdullah Alhamdi
|
|
7
|
+
Author-email: Abdullah Alhamdi <trymemain.pb.hug@gmail.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Classifier: Topic :: Utilities
|
|
15
|
+
Classifier: Environment :: Console
|
|
16
|
+
Requires-Dist: typer>=0.27.3
|
|
17
|
+
Requires-Python: >=3.14
|
|
18
|
+
Project-URL: Repository, https://github.com/cqveman/linerepeat
|
|
19
|
+
Project-URL: Issues, https://github.com/cqveman/linerepeat/issues
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# linerepeat
|
|
23
|
+
|
|
24
|
+
A small CLI tool for repeating lines from stdin or files. Useful for generating test data, creating separators, duplicating content, or any task where you need to repeat text patterns.
|
|
25
|
+
|
|
26
|
+
**Features:**
|
|
27
|
+
- **Line mode** (default): Repeat each input line N times
|
|
28
|
+
- **Block mode**: Repeat groups of lines as blocks
|
|
29
|
+
- **Custom separators**: Add delimiters between input line groups
|
|
30
|
+
- **Flexible I/O**: Read from stdin/files, write to stdout/files
|
|
31
|
+
|
|
32
|
+
## Install
|
|
33
|
+
|
|
34
|
+
### From PyPI
|
|
35
|
+
```bash
|
|
36
|
+
pip install linerepeat
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Pre-built Binaries
|
|
40
|
+
Download the latest `linerepeat` binary (Linux) or `linerepeat.exe` (Windows) from [GitHub Releases](https://github.com/cqveman/linerepeat/releases).
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
```console
|
|
45
|
+
$ linerepeat [OPTIONS] {count}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
**Arguments:**
|
|
49
|
+
|
|
50
|
+
* `count`: How many times to repeat. [required]
|
|
51
|
+
|
|
52
|
+
**Options:**
|
|
53
|
+
|
|
54
|
+
* `-i, --input <file>`: Edit this file in place instead of reading stdin.
|
|
55
|
+
* `-o, --output <file>`: Write here instead of stdout. Can't be used with --input.
|
|
56
|
+
* `-s, --separator <str>`: Line placed between repeats.
|
|
57
|
+
* `-m, --mode <line|block>`: Repeat single lines or blocks. [default: line]
|
|
58
|
+
* `-t, --till <int>`: Lines per block (block mode). [default: 0]
|
|
59
|
+
* `--help`: Show this message and exit.
|
|
60
|
+
|
|
61
|
+
## Examples
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
# Repeat each line 3 times (single line - no separator printed)
|
|
65
|
+
echo "hello" | linerepeat 3
|
|
66
|
+
# hello
|
|
67
|
+
# hello
|
|
68
|
+
# hello
|
|
69
|
+
|
|
70
|
+
# With multiple input lines, separator goes BETWEEN lines
|
|
71
|
+
echo -e "a\nb" | linerepeat 2 -s "---"
|
|
72
|
+
# a
|
|
73
|
+
# a
|
|
74
|
+
# ---
|
|
75
|
+
# b
|
|
76
|
+
# b
|
|
77
|
+
|
|
78
|
+
# Block mode with separator between blocks
|
|
79
|
+
echo -e "line1\nline2\nline3\nline4" | linerepeat 2 -m block -t 2 -s "==="
|
|
80
|
+
# line1
|
|
81
|
+
# line2
|
|
82
|
+
# ===
|
|
83
|
+
# line1
|
|
84
|
+
# line2
|
|
85
|
+
# ===
|
|
86
|
+
# line3
|
|
87
|
+
# line4
|
|
88
|
+
# ===
|
|
89
|
+
# line3
|
|
90
|
+
# line4
|
|
91
|
+
|
|
92
|
+
# In-place: duplicate every line in a file
|
|
93
|
+
linerepeat 2 -i file.txt
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## License
|
|
97
|
+
|
|
98
|
+
MIT
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
linerepeat/__init__.py,sha256=qFBngyNSMJmQkLqHmg_7Qdfsj1luWFrnoqTNAQjcnS4,69
|
|
2
|
+
linerepeat/cli.py,sha256=Wj9TrbphG4Cn9fSLmeAoz4JpffF_rEttJpEztdEfwhA,2108
|
|
3
|
+
linerepeat/repeat.py,sha256=a0MhnC4mICYXc1AF33Tupi-6XokHikWaHAWeIBZIprc,1887
|
|
4
|
+
linerepeat/stream.py,sha256=jXIkMLoY7y_u-DwDCWoCTOMk0ED9PaDa0ErEInBI4W4,2041
|
|
5
|
+
linerepeat-0.1.0.dist-info/licenses/LICENSE,sha256=CnFMEubZ7OsHq09PjFlPXpUK1a0XoWxmpvytO_38FCI,1068
|
|
6
|
+
linerepeat-0.1.0.dist-info/WHEEL,sha256=1dD_GoEqEIxQ7VnrUExgdj_eExY4b4UgfR0iBlGKVac,80
|
|
7
|
+
linerepeat-0.1.0.dist-info/entry_points.txt,sha256=hV4ekhXNleapYp0Sl2JbfDCLRTCIfYu_5GtNvFbW0S8,51
|
|
8
|
+
linerepeat-0.1.0.dist-info/METADATA,sha256=qpZi9j5Km-Mx-eohcNr6rPDskBiLhDIakf2Hf5UEv8k,2515
|
|
9
|
+
linerepeat-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright (c) 2026 Abdullah Alhamdi
|
|
2
|
+
|
|
3
|
+
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:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
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.
|