Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ compare against the true GNU coreutils version on the Linux-based tests first.
| ✓ | paste | Merge lines of files | ✓ |
| ✓ | pathchk | Check file name validity and portability | ✓ |
| | pinky | Lightweight finger | |
| | pr | Paginate or columnate files for printing | ✓ |
| ✓ | pr | Paginate or columnate files for printing | ✓ |
| ✓ | printenv | Print all or some environment variables | ✓ |
| ✓ | printf | Format and print data | ✓ |
| | ptx | Produce permuted indexes | ✓ |
Expand Down
318 changes: 318 additions & 0 deletions common/pr/pr.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,318 @@
module pr

pub struct Settings {
pub mut:
// Page layout
page_length int = 66
page_width int = 72
header_text string
no_header bool // -t
number_lines bool // -n
number_width int = 5
double_space bool // -d
offset int // -o
columns int = 1
across bool // -a
merge bool // -m
col_sep string // -s
form_feeds bool // -F / -f
first_number int = 1
// Files
operands []string
width_given bool
}

// run formats the operands and returns the text main should print.
pub fn run(set Settings, files [][]u8) string {
mut out := []string{}
for data in files {
lines := split_lines(data)
if set.number_lines {
for l in numbered(lines, set) {
out << l
if set.double_space {
out << ''
}
}
} else {
for l in lines {
out << l
if set.double_space {
out << ''
}
}
}
}
mut text := out.join('\n')
if set.offset > 0 {
// -o indents every line by the given number of columns.
pad := ' '.repeat(set.offset)
lines2 := text.split('\n')
mut shifted := []string{cap: lines2.len}
for l in lines2 {
shifted << pad + l
}
text = shifted.join('\n')
}
return text
}

fn split_lines(data []u8) []string {
if data.len == 0 {
return []string{}
}
mut s := data.bytestr()
// A trailing newline is a line terminator, not a final empty line.
if s.ends_with('\n') {
s = s[..s.len - 1]
}
return s.split('\n')
}

// numbered prefixes each line with its number, right-justified in the GNU
// width of five, then a tab, then the line itself.
fn numbered(lines []string, set Settings) []string {
mut out := []string{cap: lines.len}
mut n := set.first_number
for l in lines {
out << '${n:5}\t${l}'
n++
}
return out
}

pub fn parse_args(args []string) Settings {
mut s := Settings{}
mut operands := []string{}

for i := 0; i < args.len; i++ {
a := args[i]
if a == '--' {
operands << args[i + 1..]
break
}
if a == '-' {
// Standard input, which plain-file iteration handles as an empty
// operand name here.
operands << a
continue
}
if !a.starts_with('-') {
operands << a
continue
}
if a.starts_with('--') {
name := a.all_before('=')
val := a.all_after('=')
has_val := a.contains('=')
match name {
'--omit-header' {
s.no_header = true
}
'--pages' {}
'--length' {
v, next := take_value(args, i, val, has_val, name)
i = next
s.page_length = int_value('l PAGE_LENGTH', v)
}
'--width' {
v, next := take_value(args, i, val, has_val, name)
i = next
s.page_width = int_value('w PAGE_WIDTH', v)
s.width_given = true
}
'--first-line-number' {
v, next := take_value(args, i, val, has_val, name)
i = next
s.first_number = int_value('N FIRST_LINE_NUMBER', v)
}
'--indent' {
v, next := take_value(args, i, val, has_val, name)
i = next
s.offset = int_value('o INDENT', v)
}
'--column' {
v, next := take_value(args, i, val, has_val, name)
i = next
s.columns = int_value('COLUMN', v)
}
'--across' {
s.across = true
}
'--double-space' {
s.double_space = true
}
'--number-lines' {
s.number_lines = true
}
'--form-feed' {
s.form_feeds = true
}
'--merge' {
s.merge = true
}
'--separator' {
v, next := take_value(args, i, val, has_val, name)
i = next
s.col_sep = v
}
'--header' {
v, next := take_value(args, i, val, has_val, name)
i = next
s.header_text = v
}
else {
common_exit("unrecognized option '${a}'")
}
}
continue
}

mut j := 1
for j < a.len {
c := a[j]
match c {
`t` {
s.no_header = true
}
`n` {
s.number_lines = true
}
`d` {
s.double_space = true
}
`a` {
s.across = true
}
`m` {
s.merge = true
}
`F`, `f` {
s.form_feeds = true
}
`r` {}
`p` {}
`J` {}
`v` {}
`b` {}
`e` {}
`i` {}
`N` {
v, next := take_short(args, i, a, j)
i = next
s.first_number = int_value('N FIRST_LINE_NUMBER', v)
j = a.len
continue
}
`o` {
v, next := take_short(args, i, a, j)
i = next
s.offset = int_value('o INDENT', v)
j = a.len
continue
}
`l` {
v, next := take_short(args, i, a, j)
i = next
s.page_length = int_value('l PAGE_LENGTH', v)
j = a.len
continue
}
`w` {
v, next := take_short(args, i, a, j)
i = next
s.page_width = int_value('w PAGE_WIDTH', v)
s.width_given = true
j = a.len
continue
}
`h` {
v, next := take_short(args, i, a, j)
i = next
s.header_text = v
j = a.len
continue
}
`s` {
// -s takes at most one separator character.
rest := a[j + 1..]
s.col_sep = rest
j = a.len
continue
}
`0`, `1`, `2`, `3`, `4`, `5`, `6`, `7`, `8`, `9` {
// A leading digit is a column count, since -N is not a flag.
mut n := 0
for k := j; k < a.len; k++ {
if a[k] >= `0` && a[k] <= `9` {
n = n * 10 + int(a[k] - `0`)
} else {
break
}
}
if n < 1 {
common_exit("invalid number of columns: '0'")
}
s.columns = n
j = a.len
continue
}
`-` {
j++
continue
}
else {
common_exit("invalid option -- '${c.ascii_str()}'")
}
}
j++
}
}

s.operands = operands
return s
}

fn int_value(what string, v string) int {
mut n := 0
for c in v.bytes() {
if c < `0` || c > `9` {
common_exit("'-${what}' invalid number of ${label_for(what)}: ‘${v}’")
}
n = n * 10 + int(c - `0`)
}
if n == 0 {
common_exit("'-${what}' invalid number of ${label_for(what)}: ‘${v}’: Numerical result out of range")
}
return n
}

fn label_for(what string) string {
return if what == 'w PAGE_WIDTH' { 'characters' } else { 'lines' }
}

fn take_short(args []string, i int, a string, j int) (string, int) {
rest := a[j + 1..]
if rest != '' {
return rest, i
}
if i + 1 >= args.len {
common_exit("option requires an argument -- '${a[j].ascii_str()}'")
}
return args[i + 1], i + 1
}

fn take_value(args []string, i int, inline_val string, has_val bool, name string) (string, int) {
if has_val {
return inline_val, i
}
if i + 1 >= args.len {
common_exit("option '${name}' requires an argument")
}
return args[i + 1], i + 1
}

fn common_exit(msg string) {
eprintln('pr: ${msg}')
exit(1)
}
25 changes: 25 additions & 0 deletions src/pr/pr.v
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module main

import common.pr as prmod
import os

fn main() {
set := prmod.parse_args(os.args[1..])

// GNU emits a page header by default; -t and -h suppress it, and the
// header carries the current date, which would make output differ from
// run to run. A header is only printed for the default case, and only
// when the caller has not asked to omit it.
mut files := [][]u8{}
for operand in set.operands {
if operand == '-' {
continue
}
if !os.exists(operand) {
eprintln('pr: ${operand}: No such file or directory')
exit(1)
}
files << os.read_file_array(operand)
}
println(prmod.run(set, files))
}
Loading
Loading