Compare commits

..

No commits in common. "v0.1.0" and "main" have entirely different histories.
v0.1.0 ... main

22 changed files with 813 additions and 471 deletions

View file

@ -0,0 +1,52 @@
name: Rust
on:
push:
branches: ["next"]
pull_request:
branches: ["main"]
env:
CARGO_TERM_COLOR: always
jobs:
build:
runs-on: docker
strategy:
matrix:
toolchain:
- name: stable
- name: nightly
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Format
uses: https://git.kemitix.net/kemitix/rust@v2.1.0
with:
args: ${{ matrix.toolchain.name }} cargo fmt --all -- --check
# - name: Machete
# uses: https://github.com/bnjbvr/cargo-machete@v0.6.2
- name: Clippy
uses: https://git.kemitix.net/kemitix/rust@v2.1.0
with:
args: ${{ matrix.toolchain.name }} cargo clippy
- name: Build
uses: https://git.kemitix.net/kemitix/rust@v2.1.0
with:
args: ${{ matrix.toolchain.name }} cargo build
- name: Test
uses: https://git.kemitix.net/kemitix/rust@v2.1.0
with:
args: ${{ matrix.toolchain.name }} cargo test --no-fail-fast
- name: Integration
uses: https://git.kemitix.net/kemitix/rust@v2.1.0
with:
args: ./test.sh

6
.gitattributes vendored
View file

@ -1,6 +0,0 @@
* text=auto
*.zig text eol=lf
zig.mod text eol=lf
zigmod.* text eol=lf
zig.mod linguist-language=YAML
zig.mod gitlab-language=yaml

View file

@ -1,27 +0,0 @@
name: CI
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
zig: [ 0.9.0, master]
steps:
- uses: actions/checkout@v2
- name: Setup Zig
uses: goto-bus-stop/setup-zig@v1.3.0
with:
version: ${{ matrix.zig }}
- run: zig version
- run: zig env
- uses: nektro/actions-setup-zigmod@v1
- run: zigmod ci
- run: zig build test
- run: zig build
- run: cp zig-out/bin/skip .
- run: ./test.sh

10
.gitignore vendored
View file

@ -1,7 +1,3 @@
zig-cache/
zig-out/
.zigmod
deps.zig
/test.in
/test.out
/test.expect
/target
dist/
input.txt

46
.woodpecker.yml Normal file
View file

@ -0,0 +1,46 @@
variables:
- &rust_image "docker.io/rust:1.81.0"
- &slow_check_paths
- path:
# rust source code
- "crates/**"
- "src/**"
- "tests/**"
- "**/Cargo.toml"
- "Cargo.lock"
# database migrations
- "migrations/**"
# config files and scripts used by ci
- ".woodpecker.yml"
steps:
toml_fmt:
image: docker.io/tamasfe/taplo:0.9.3
commands:
- taplo format --check
cargo_machete:
image: *rust_image
commands:
- wget https://github.com/cargo-bins/cargo-binstall/releases/latest/download/cargo-binstall-x86_64-unknown-linux-musl.tgz
- tar -xvf cargo-binstall-x86_64-unknown-linux-musl.tgz
- rm cargo-binstall-x86_64-unknown-linux-musl.tgz
- mv cargo-binstall /usr/local/cargo/bin
- cargo binstall -y cargo-machete
- cargo machete
ignored_files:
image: docker.io/alpine:latest
commands:
- apk add git
- IGNORED=$(git ls-files --cached -i --exclude-standard)
- if [[ "$IGNORED" ]]; then echo "Ignored files present:\n$IGNORED\n"; exit 1; fi
publish_to_crates_io:
image: *rust_image
commands:
- cargo login "$CARGO_REGISTRY_TOKEN"
- cargo publish --registry crates-io --no-verify
secrets: [cargo_registry_token]
when:
event: tag

223
Cargo.lock generated Normal file
View file

@ -0,0 +1,223 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "anstream"
version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e1ebcb11de5c03c67de28a7df593d32191b44939c482e97702baaaa6ab6a5"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87"
[[package]]
name = "anstyle-parse"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648"
dependencies = [
"windows-sys",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7"
dependencies = [
"anstyle",
"windows-sys",
]
[[package]]
name = "clap"
version = "4.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80c21025abd42669a92efc996ef13cfb2c5c627858421ea58d5c3b331a6c134f"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458bf1f341769dfcf849846f65dffdf9146daa56bcd2a47cb4e1de9915567c99"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "307bc0538d5f0f83b8248db3087aa92fe504e4691294d0c96c0eabc33f47ba47"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "clap_lex"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce"
[[package]]
name = "colorchoice"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7"
[[package]]
name = "heck"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
[[package]]
name = "proc-macro2"
version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc"
dependencies = [
"proc-macro2",
]
[[package]]
name = "skip"
version = "0.2.1"
dependencies = [
"clap",
]
[[package]]
name = "strsim"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01"
[[package]]
name = "syn"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79d9531f94112cfc3e4c8f5f02cb2b58f72c97b7efd85f70203cc6d8efda5927"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4"
[[package]]
name = "utf8parse"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a"
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef"
[[package]]
name = "windows_i686_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313"
[[package]]
name = "windows_i686_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04"

17
Cargo.toml Normal file
View file

@ -0,0 +1,17 @@
[package]
name = "skip"
version = "0.2.1"
edition = "2021"
authors = ["Paul Campbell <pcampbell@kemitix.net>"]
categories = ["command-line-utilities"]
description = "Skip lines in a file"
license = "MIT"
repository = "https://git.kemitix.net/kemitix/skip"
keywords = ["skip", "lines", "file", "text", "utility"]
rust-version = "1.74.1"
exclude = [".cargo_home"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
clap = { version = "4.5", features = ["derive"] }

21
LICENSE
View file

@ -1,21 +0,0 @@
MIT License
Copyright (c) 2021 Paul Campbell
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:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
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.

101
README.md
View file

@ -2,34 +2,29 @@
Skip part of a file.
![GitHub release (latest by date)](
https://img.shields.io/github/v/release/kemitix/skip?style=for-the-badge)
![GitHub Release Date](
https://img.shields.io/github/release-date/kemitix/skip?style=for-the-badge)
As `head` will show the top of a file after a number of line,
As `head` will show the top of a file up-to a number of line,
so `skip` will do the opposite, and not show the top of the file,
but will show the rest.
Additionally, it can check for whole lines matching,
or for a token being present on the line.
N.B.: The `skip` crate used to be an implementation of [Skip list](https://en.wikipedia.org/wiki/Skip_list),
by [Luo Jia / Zhouqi Jiang](https://github.com/luojia65) ([source](https://github.com/luojia65/skip)).
That crate will be republished as [skip-list](https://crates.io/crates/skip-list) (soon).
## Usage
### Skip a fixed number of lines
This example reads the file from stdin.
File: `input.txt`
```text
line 1
```bash
echo "line 1
line 2
line 3
line 4
```
line 4" > input.txt
```bash
skip 2 < input.txt
```
@ -42,20 +37,18 @@ line 4
### Skip until a number of matching lines
The whole line must match.
This example reads the named file.
File: `input.txt`
```text
alpha
```bash
echo "alpha
beta
alpha
alpha
gamma
alpha
```
alpha" > input.txt
```bash
skip 2 --line alpha input.txt
```
@ -67,24 +60,22 @@ gamma
alpha
```
### Skip lines until a number of tokens as seen
### Skip lines until a number of tokens are seen
Looks for a string within a line, counting each occurance.
This example reads the file from stdin.
File: `input.txt`
```text
Lorem ipsum dolor sit amet,
```bash
echo "Lorem ipsum dolor sit amet,
consectetur adipiscing elit,
sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua.
Ut enim ad minim veniam,
quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea
commodo consequat.
```
commodo consequat." > input.txt
```bash
cat input.txt | skip 2 --token dolor
```
@ -94,8 +85,60 @@ Will output:
Ut enim ad minim veniam,
quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea
commodo consequat.
commodo consequat.
```
It matches the first `dolor` on line 1,
and the second on line 4 as part of the word `dolore`.
### Skip lines until a lines with tokens are seen
Looks for a string within a line, only counting each matching line once.
This example reads the file from stdin.
```bash
echo "Lorem ipsum dolor sit amet,
consectetur adipiscing elit,
sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua.
Ut enim ad minim veniam,
quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea
commodo consequat." > input.txt
cat input.txt | skip 4 --token m --ignore-extras
```
Will output:
```text
quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea
commodo consequat.
```
Without `--ignore-extras`, it would have found the fourth `m` on line 3.
```bash
echo "Lorem ipsum dolor sit amet,
consectetur adipiscing elit,
sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua.
Ut enim ad minim veniam,
quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea
commodo consequat." > input.txt
cat input.txt | skip 4 --token m
```
Outputing:
```text
ut labore et dolore magna aliqua.
Ut enim ad minim veniam,
quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea
commodo consequat.
```

View file

@ -1,35 +0,0 @@
const std = @import("std");
const deps = @import("./deps.zig");
pub fn build(b: *std.build.Builder) void {
// Standard target options allows the person running `zig build` to choose
// what target to build for. Here we do not override the defaults, which
// means any target is allowed, and the default is native. Other options
// for restricting supported target set are available.
const target = b.standardTargetOptions(.{});
// Standard release options allow the person running `zig build` to select
// between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall.
const mode = b.standardReleaseOptions();
const exe = b.addExecutable("skip", "src/main.zig");
exe.setTarget(target);
exe.setBuildMode(mode);
deps.addAllTo(exe);
exe.install();
const run_cmd = exe.run();
run_cmd.step.dependOn(b.getInstallStep());
if (b.args) |args| {
run_cmd.addArgs(args);
}
const run_step = b.step("run", "Run the app");
run_step.dependOn(&run_cmd.step);
const exe_tests = b.addTest("src/main.zig");
exe_tests.setBuildMode(mode);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&exe_tests.step);
}

15
justfile Normal file
View file

@ -0,0 +1,15 @@
dist: target-release
if test ! -d dist ; then mkdir dist ; fi
cp target/release/skip dist/
target-release: unittest inttest
cargo build --release
inttest: target-debug
./test.sh
unittest: target-debug
cargo test
target-debug:
cargo build

10
renovate.json Normal file
View file

@ -0,0 +1,10 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"packageRules": [
{
"matchManagers": ["cargo"],
"rangeStrategy": "replace"
}
]
}

263
src/lib.rs Normal file
View file

@ -0,0 +1,263 @@
use clap::Parser;
use std::fmt::Debug;
use std::fs::File;
use std::io::{BufRead, BufReader, Write};
use std::path::PathBuf;
pub type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
/// Skip lines at the start of a file
#[derive(Parser, Debug)]
#[command(version)]
pub struct Cli {
/// The number of lines (or tokens) to skip
lines: usize,
/// The file to read, or stdin if not given
pub file: Option<PathBuf>,
/// Skip until N lines matching this
#[arg(short, long, conflicts_with = "token")]
line: Option<String>,
/// Skip lines until N tokens found
#[arg(
short,
long,
conflicts_with = "line",
required_if_eq("ignore_extras", "true")
)]
token: Option<String>,
/// Only count the first token on each line
#[arg(short, long = "ignore-extras")]
ignore_extras: bool,
}
pub fn skip(cli: &Cli, writer: &mut impl Write) -> Result<()> {
let reader: Box<dyn BufRead> = match &cli.file {
Some(ref file) => {
let file = File::open(file)?;
Box::new(BufReader::new(file))
}
None => Box::new(BufReader::new(std::io::stdin())),
};
if let Some(line) = &cli.line {
skip_lines_matching(cli, reader, writer, line)
} else if let Some(ref token) = cli.token {
skip_tokens(cli, reader, writer, token)
} else {
skip_lines(cli, reader, writer)
}
}
// skip a number of lines
fn skip_lines(cli: &Cli, reader: Box<dyn BufRead>, writer: &mut impl Write) -> Result<()> {
for (counter, current_line) in reader.lines().map_while(Option::Some).flatten().enumerate() {
if counter >= cli.lines {
writeln!(writer, "{}", current_line)?;
}
}
Ok(())
}
// skip until a number of matching lines seen
fn skip_lines_matching(
cli: &Cli,
reader: Box<dyn BufRead>,
writer: &mut impl Write,
line: &str,
) -> Result<()> {
let mut counter = 0usize;
for current_line in reader.lines().map_while(Option::Some).flatten() {
if counter >= cli.lines {
writeln!(writer, "{}", current_line)?;
}
if line == current_line {
counter += 1;
}
}
Ok(())
}
// skip until a number of tokens seen
// may or may not count each occurance of token on a line - see cli.ignore_extras
fn skip_tokens(
cli: &Cli,
reader: Box<dyn BufRead>,
writer: &mut impl Write,
token: &str,
) -> Result<()> {
let mut counter = 0usize;
for current_line in reader.lines().map_while(Option::Some).flatten() {
if counter >= cli.lines {
writeln!(writer, "{}", current_line)?;
}
if current_line.contains(token) {
if cli.ignore_extras {
counter += 1;
} else {
let occurances = current_line.matches(&token).count();
counter += occurances;
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn skip_one_line() -> Result<()> {
//given
let cli = Cli {
lines: 1,
file: Some(PathBuf::from("tests/two-lines.txt")),
line: None,
token: None,
ignore_extras: false,
};
let mut lines = Vec::new();
//when
skip(&cli, &mut lines)?;
//then
assert_eq!(String::from_utf8(lines)?, "line 2\n");
Ok(())
}
#[test]
fn skip_two_lines() -> Result<()> {
//given
let cli = Cli {
lines: 2,
file: Some(PathBuf::from("tests/four-lines.txt")),
line: None,
token: None,
ignore_extras: false,
};
let mut lines = Vec::new();
//when
skip(&cli, &mut lines)?;
//then
assert_eq!(String::from_utf8(lines)?, ["alpha", "gamma\n"].join("\n"));
Ok(())
}
#[test]
fn skip_two_matching_lines() -> Result<()> {
//given
let cli = Cli {
lines: 2,
file: Some(PathBuf::from("tests/four-lines.txt")),
line: Some(String::from("alpha")),
token: None,
ignore_extras: false,
};
let mut lines = Vec::new();
//when
skip(&cli, &mut lines)?;
//then
assert_eq!(String::from_utf8(lines)?, "gamma\n");
Ok(())
}
#[test]
fn skip_three_matching_tokens() -> Result<()> {
//given
let cli = Cli {
lines: 3,
file: Some(PathBuf::from("tests/poem.txt")),
line: None,
token: Some(String::from("one")),
ignore_extras: false,
};
let mut lines = Vec::new();
//when
skip(&cli, &mut lines)?;
//then
assert_eq!(
String::from_utf8(lines)?,
[
"Or help one fainting robin",
"Unto his nest again,",
"I shall not live in vain.\n"
]
.join("\n")
);
Ok(())
}
#[test]
fn skip_three_matching_tokens_include_extras() -> Result<()> {
//given
let cli = Cli {
lines: 4,
file: Some(PathBuf::from("tests/lorem.txt")),
line: None,
token: Some(String::from("or")),
ignore_extras: false,
};
let mut lines = Vec::new();
//when
skip(&cli, &mut lines)?;
//then
assert_eq!(
String::from_utf8(lines)?,
[
//Lorem ipsum dolor sit amet, -- +2 = 2
//consectetur adipiscing elit,
//sed do eiusmod tempor incididunt -- +1 = 3
//ut labore et dolore magna aliqua. -- +2 = 5
"Ut enim ad minim veniam,",
"quis nostrud exercitation ullamco",
"laboris nisi ut aliquip ex ea",
"commodo consequat.\n"
]
.join("\n")
);
Ok(())
}
#[test]
fn skip_three_matching_tokens_ignore_extras() -> Result<()> {
//given
let cli = Cli {
lines: 4,
file: Some(PathBuf::from("tests/lorem.txt")),
line: None,
token: Some(String::from("or")),
ignore_extras: true,
};
let mut lines = Vec::new();
//when
skip(&cli, &mut lines)?;
//then
assert_eq!(
String::from_utf8(lines)?,
[
//Lorem ipsum dolor sit amet, -- 1
//consectetur adipiscing elit,
//sed do eiusmod tempor incididunt -- 2
//ut labore et dolore magna aliqua. -- 3
//Ut enim ad minim veniam,
//quis nostrud exercitation ullamco
//laboris nisi ut aliquip ex ea -- 4
"commodo consequat.\n"
]
.join("\n")
);
Ok(())
}
}

8
src/main.rs Normal file
View file

@ -0,0 +1,8 @@
use clap::Parser;
use skip::{skip, Cli, Result};
fn main() -> Result<()> {
let cli = Cli::parse();
let mut stdout = std::io::stdout();
skip(&cli, &mut stdout)
}

View file

@ -1,321 +0,0 @@
const std = @import("std");
const builtin = @import("builtin");
const os = builtin.os;
const fmt = std.fmt;
const mem = std.mem;
const testing = std.testing;
const io = std.io;
const heap = std.heap;
const fs = std.fs;
const clap = @import("clap");
const version = "0.1.0";
// step 1: [x] read in a file from stdin and write out to stdout
// step 2: [x] read in a named file in parameters and write out to stdout
// step 3: [x] skip a number of lines
// step 4: [ ] skip a number of matching lines
// step 5: [ ] skip a number of tokens
const maxLineLength = 4096;
pub fn main() anyerror!void {
var buffer: [maxLineLength]u8 = undefined;
var fba = heap.FixedBufferAllocator.init(&buffer);
const allocator = fba.allocator();
const config: Config = parseArgs(allocator) catch |err| switch (err) {
error.EarlyExit => return,
error.FileNotFound => return,
error.BadArgs => return,
else => @panic("Unknown error"),
};
defer config.deinit();
const stdout = io.getStdOut();
if (config.file) |file| {
try dumpInput(config, file, stdout, allocator);
} else {
const stdin = io.getStdIn();
try dumpInput(config, stdin, stdout, allocator);
}
}
const errors = error {
EarlyExit,
FileNotFound,
BadArgs,
};
const Config = struct {
lines: u32,
file: ?fs.File,
line: ?[]const u8 = null,
token: ?[]const u8 = null,
pub fn deinit(self: @This()) void {
if (self.file) |f| {
f.close();
}
}
};
fn parseArgs(allocator: mem.Allocator) !Config {
const params = comptime [_]clap.Param(clap.Help) {
clap.parseParam("<N> The number of lines to skip") catch unreachable,
clap.parseParam("[<FILE>] The file to read or stdin if not given") catch unreachable,
clap.parseParam("-l, --line <STR> Skip until N lines matching this") catch unreachable,
clap.parseParam("-t, --token <STR> Skip lines until N tokens found") catch unreachable,
clap.parseParam("-h, --help Display this help and exit") catch unreachable,
clap.parseParam("-v, --version Display the version") catch unreachable,
};
var diag = clap.Diagnostic{};
var args = clap.parse(clap.Help, &params, .{ .diagnostic = &diag }) catch |err| {
diag.report(io.getStdErr().writer(), err) catch {};
return err;
};
defer args.deinit();
if (args.flag("--version")) {
std.debug.print("skip version {s}\n", .{ version });
return error.EarlyExit;
}
if (args.flag("--help")) {
try io.getStdErr().writer().print("skip <N> [<FILE>] [-h|--help] [-v|--version]\n", .{});
try clap.help(io.getStdErr().writer(), &params);
return error.EarlyExit;
}
if (args.option("--line")) |_| {
if (args.option("--token")) |_| {
try io.getStdErr().writer().print("Error: only specify one of --line or --token, not both\n", .{});
return error.BadArgs;
}
}
var line: ?[]const u8 = null;
if (args.option("--line")) |match| {
line = try allocator.dupe(u8, match);
}
var token: ?[]const u8 = null;
if (args.option("--token")) |match| {
token = try allocator.dupe(u8, match);
}
var n: u32 = 0;
var file: ?fs.File = null;
if (args.positionals().len == 0) {
std.debug.print("Number of lines to skip not given. Try skip --help\n", .{});
return error.EarlyExit;
}
if (args.positionals().len >= 1) {
n = try fmt.parseInt(u32, args.positionals()[0], 10);
}
if (args.positionals().len >= 2) {
const filename = args.positionals()[1];
file = fs.cwd().openFile(filename, .{ .read = true, .write = false }) catch |err| switch (err) {
error.FileNotFound => {
try io.getStdErr().writer().print("Error: File not found: {s}\n", .{ filename });
return err;
},
else => return err,
};
}
return Config {
.lines = n,
.file = file,
.line = line,
.token = token,
};
}
fn dumpInput(config: Config, in: fs.File, out: fs.File, allocator: mem.Allocator) !void {
const writer = out.writer();
const reader = in.reader();
var it: LineIterator = lineIterator(reader, allocator);
var c: usize = 0;
while (c < config.lines) {
const line = it.next();
if (config.line) |match| {
if (line) |memory| {
if (mem.eql(u8, match, memory)) {
c += 1;
}
}
} else {
if (config.token) |token| {
if (line) |memory| {
c += mem.count(u8, memory, token);
}
} else {
c += 1;
}
}
if (line) |memory| {
allocator.free(memory);
} else return;
}
try pumpIterator(&it, writer, allocator);
}
test "dumpInput skip 1 line" {
const file = try fs.cwd().openFile("src/test/two-lines.txt", .{ .read = true, .write = false });
defer file.close();
const tempFile = "zig-cache/test.txt";
const output = try fs.cwd().createFile(tempFile, .{});
defer output.close();
const config = Config{
.lines = 1,
.file = file,
};
try dumpInput(config, file, output, testing.allocator);
const result = try fs.cwd().openFile(tempFile, .{ .read = true });
defer result.close();
var rit = lineIterator(result.reader(), testing.allocator);
const line1 = rit.next().?;
defer testing.allocator.free(line1);
try testing.expectEqualStrings("line 2", line1);
const eof = rit.next();
try testing.expect(eof == null);
}
test "dumpInput skip 2 line 'alpha'" {
const file = try fs.cwd().openFile("src/test/four-lines.txt", .{ .read = true, .write = false });
defer file.close();
const tempFile = "zig-cache/test.txt";
const output = try fs.cwd().createFile(tempFile, .{});
defer output.close();
const config = Config{
.lines = 2,
.file = file,
.line = "alpha",
};
try dumpInput(config, file, output, testing.allocator);
const result = try fs.cwd().openFile(tempFile, .{ .read = true });
defer result.close();
var rit = lineIterator(result.reader(), testing.allocator);
const line1 = rit.next().?;
defer testing.allocator.free(line1);
try testing.expectEqualStrings("gamma", line1);
const eof = rit.next();
try testing.expect(eof == null);
}
fn pumpIterator(it: *LineIterator, writer: fs.File.Writer, allocator: mem.Allocator) !void {
while (it.next()) |line| {
defer allocator.free(line);
try writer.print("{s}\n", .{ windowsSafe(line) });
}
}
test "pumpIterator" {
const file = try fs.cwd().openFile("src/test/two-lines.txt", .{ .read = true, .write = false });
defer file.close();
const tempFile = "zig-cache/test.txt";
const output = try fs.cwd().createFile(tempFile, .{});
defer output.close();
var reader = file.reader();
var it = lineIterator(reader, testing.allocator);
var writer = output.writer();
try pumpIterator(&it, writer, testing.allocator);
const result = try fs.cwd().openFile(tempFile, .{ .read = true });
defer result.close();
var rit = lineIterator(result.reader(), testing.allocator);
const line1 = rit.next().?;
defer testing.allocator.free(line1);
try testing.expectEqualStrings("line 1", line1);
const line2 = rit.next().?;
defer testing.allocator.free(line2);
try testing.expectEqualStrings("line 2", line2);
const eof = rit.next();
try testing.expect(eof == null);
}
const LineIterator = struct {
reader: io.BufferedReader(maxLineLength, fs.File.Reader),
delimiter: u8,
allocator: mem.Allocator,
const Self = @This();
/// Caller owns returned memory
pub fn next(self: *Self) ?[]u8 {
return self.reader.reader().readUntilDelimiterOrEofAlloc(self.allocator, self.delimiter, maxLineLength) catch null;
}
};
fn lineIterator(reader: fs.File.Reader, allocator: mem.Allocator) LineIterator {
return LineIterator {
.reader = io.bufferedReader(reader),
.delimiter = '\n',
.allocator = allocator
};
}
test "lineIterator returns lines in buffer" {
const file = try fs.cwd().openFile("src/test/two-lines.txt", .{ .read = true, .write = false });
defer file.close();
var reader = file.reader();
var it = lineIterator(reader, testing.allocator);
const line1 = it.next().?;
defer testing.allocator.free(line1);
try testing.expectEqualStrings("line 1", line1);
const line2 = it.next().?;
defer testing.allocator.free(line2);
try testing.expectEqualStrings("line 2", line2);
const eof = it.next();
try testing.expect(eof == null);
}
fn windowsSafe(line: []const u8) []const u8 {
// trim annoying windows-only carriage return character
if (os.tag == .windows) {
return mem.trimRight(u8, line, "\r");
}
return line;
}
test "windowsSafe strips carriage return on windows" {
const input = "line\n\r";
const result = windowsSafe(input);
if (os.tag == .windows) {
// strips the carriage return if windows
try testing.expectEqualSlices(u8, "line\n", result);
} else {
// doesn't change the line if not windows
try testing.expectEqualSlices(u8, input, result);
}
}

106
test.sh
View file

@ -2,27 +2,41 @@
set -e
echo "PWD: $PWD"
ls -l
ls -l target
SKIP="./target/debug/skip"
DIFF="diff -u --color"
if test ! -x $SKIP; then
echo "File missing: $SKIP - try 'cargo build'"
exit 1
fi
echo "> skip a line when reading from stdin"
INPUT=$(cat<<EOF
INPUT=$(
cat <<EOF
line 1
line 2
EOF
)
echo "line 2" > test.expect
echo "$INPUT" | ./skip 1 > test.out
diff --brief test.expect test.out
echo "line 2" >test.expect
echo "$INPUT" | $SKIP 1 >test.out
$DIFF test.expect test.out
rm test.expect test.out
echo "> skip a line when reading from a file"
cat<<EOF > test.in
cat <<EOF >test.in
line 1
line 2
EOF
echo "line 2" > test.expect
./skip 1 test.in > test.out
diff --brief test.expect test.out
echo "line 2" >test.expect
$SKIP 1 test.in >test.out
$DIFF test.expect test.out
rm test.expect test.out
echo "> skip until 2 matching lines seen"
cat<<EOF > test.in
cat <<EOF >test.in
alpha
beta
alpha
@ -30,16 +44,17 @@ alpha
gamma
alpha
EOF
cat<<EOF > test.expect
cat <<EOF >test.expect
alpha
gamma
alpha
EOF
./skip 2 test.in --line alpha > test.out
diff --brief test.expect test.out
$SKIP 2 test.in --line alpha >test.out
$DIFF test.expect test.out
rm test.in test.expect test.out
echo "> skip lines until 2 tokens seen"
cat<<EOF > test.in
cat <<EOF >test.in
Lorem ipsum dolor sit amet,
consectetur adipiscing elit,
sed do eiusmod tempor incididunt
@ -49,15 +64,72 @@ quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea
commodo consequat.
EOF
cat<<EOF > test.expect
cat <<EOF >test.expect
Ut enim ad minim veniam,
quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea
commodo consequat.
EOF
./skip 2 test.in --token dolor > test.out
diff --brief test.expect test.out
$SKIP 2 test.in --token dolor >test.out
$DIFF test.expect test.out
rm test.in test.expect test.out
rm test.in test.out test.expect
echo "> handle unknown parameter with simple error message"
cat <<EOF >test.expect.err
error: unexpected argument '--foo' found
tip: to pass '--foo' as a value, use '-- --foo'
Usage: skip [OPTIONS] <LINES> [FILE]
For more information, try '--help'.
EOF
cat <<EOF >test.expect
EOF
touch test.out test.err
$SKIP --foo >test.out 2>test.err || true
$DIFF test.expect test.out
$DIFF test.expect.err test.err
rm test.expect test.out
rm test.expect.err test.err
echo "> handle ignore-extra when token is missing"
cat <<EOF >test.expect.err
error: the following required arguments were not provided:
--token <TOKEN>
<LINES>
Usage: skip --ignore-extras --token <TOKEN> <LINES> [FILE]
For more information, try '--help'.
EOF
cat <<EOF >test.expect
EOF
touch test.out test.err
$SKIP --ignore-extras >test.out 2>test.err || true
$DIFF test.expect test.out
$DIFF test.expect.err test.err
rm test.expect test.out
rm test.expect.err test.err
echo "> skip lines until 4 tokens seen - ignored extra tokens on same line"
cat <<EOF >test.in
Lorem ipsum dolor sit amet,
consectetur adipiscing elit,
sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua.
Ut enim ad minim veniam,
quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea
commodo consequat.
EOF
cat <<EOF >test.expect
quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea
commodo consequat.
EOF
$SKIP 4 test.in --token m --ignore-extras >test.out
$DIFF test.expect test.out
rm test.in test.expect test.out
echo done

8
tests/lorem.txt Normal file
View file

@ -0,0 +1,8 @@
Lorem ipsum dolor sit amet,
consectetur adipiscing elit,
sed do eiusmod tempor incididunt
ut labore et dolore magna aliqua.
Ut enim ad minim veniam,
quis nostrud exercitation ullamco
laboris nisi ut aliquip ex ea
commodo consequat.

7
tests/poem.txt Normal file
View file

@ -0,0 +1,7 @@
If I can stop one heart from breaking,
I shall not live in vain;
If I can ease one life the aching,
Or cool one pain,
Or help one fainting robin
Unto his nest again,
I shall not live in vain.

View file

@ -1,6 +0,0 @@
id: va9657y7sc352r94jivh9b17lnaffyrou0dlu3x45n5ifm2q
name: skip
license: MIT
description: skip part of a file
dev_dependencies:
- src: git https://github.com/Hejsil/zig-clap

View file

@ -1,2 +0,0 @@
2
git https://github.com/Hejsil/zig-clap commit-802a04a854e65254b0632d9785b2b0f6154686b8