Compare commits

...

25 Commits
0.13.0 ... pg

Author SHA1 Message Date
DTB
7142994367 pg(1): start work on a quoted strtok(3) and internal environment 2024-04-26 19:08:48 -06:00
DTB
eb454a6dad pg(1): Simplify exit statuses from subcommands 2024-04-23 19:39:25 -06:00
DTB
9c1e0d785d pg(1): add -p option 2024-04-22 07:52:41 -06:00
DTB
68919bf877 pg(1): put command system in 2024-04-22 01:12:02 -06:00
DTB
ea8b58554e pg(1): make it fit the project better 2024-04-21 06:30:01 -06:00
DTB
e26d8e0e1b pg(1): import from trinity/src 2024-04-21 06:21:15 -06:00
8d9ac33566 Merge branch 'strerror' 2024-03-29 19:23:27 -06:00
b503d97625 Makefile: directory specification changes 2024-03-29 19:22:55 -06:00
b562898a74 copyright 2024-03-27 09:21:19 +05:45
d05d2fae05 Makefile: add a missing build target for npc(1) 2024-03-26 17:19:21 +05:45
d87b5d0958 hru(1), fop(1): changed to reflect strerror changes 2024-03-18 21:31:25 -06:00
1891c3e1aa strerror(3): created strerror() method 2024-03-18 21:30:43 -06:00
127192185f Merge branch 'dj-fix' (closes #66) 2024-03-18 21:01:48 -06:00
b356ac522f strerror(3): changed panic to 0 2024-03-18 20:45:53 -06:00
b2d56bbc9a Makefile: even more changes 2024-03-09 22:02:19 -07:00
2ad7140e1e Makefile: DESTDIR 2024-03-09 11:53:03 -07:00
8193e471f0 changes redundant .unwrap_or_else(panic!()) to .unwrap() 2024-03-03 17:26:40 -07:00
c392dbc680 Makefile: better deps; fop(1), hru(1), strerror(3): changed strerror wrapper function name 2024-03-02 10:16:32 -07:00
898044cd43 Makefile: better macro assignment 2024-03-01 23:51:36 -07:00
ca01ca4074 rpn(1): remove erroneous strerror(3) dependency 2024-03-01 23:14:58 -07:00
3e6dc5cc46 rpn(1): make error messages consistent with other tools 2024-03-01 23:14:18 -07:00
cf1d16f860 hru(1): changed to use strerror(3) 2024-03-01 23:12:44 -07:00
8f956d775c fop(1): changed to use strerror(3) 2024-03-01 23:10:28 -07:00
e81703c6e1 sterror(3): added library for C-like error messages; Makefile: some efficiency changes 2024-03-01 23:04:53 -07:00
DTB
58245c9484 dj(1): remove function pointer hijinks (fixes #66) 2024-02-22 19:27:14 -07:00
9 changed files with 431 additions and 66 deletions

View File

@@ -1,6 +1,7 @@
# Copyright (c) 20232024 Emma Tebibyte <emma@tebibyte.media>
# Copyright (c) 20232024 DTB <trinity@trinity.moe>
# Copyright (c) 2023 Sasha Koshka <sashakoshka@tebibyte.media>
# Copyright (c) 2024 Aaditya Aryal <aryalaadi123@gmail.com>
# SPDX-License-Identifier: FSFAP
#
# Copying and distribution of this file, with or without modification, are
@@ -8,16 +9,26 @@
# notice are preserved. This file is offered as-is, without any warranty.
.POSIX:
# if using BSD make(1), remove these pragmas because they break it
.PRAGMA: posix_202x # future POSIX standard support à la pdpmake(1)
.PRAGMA: command_comment # breaks without this?
PREFIX=/usr/local
DESTDIR ?= dist
PREFIX ?= /usr/local
CC=cc
RUSTC=rustc
SYSEXITS != printf '\043include <sysexits.h>\n' | cpp -M - | sed 's/ /\n/g' \
| sed -n 's/sysexits\.h//p' || printf 'include\n'
CC ?= cc
RUSTC ?= rustc
RUSTLIBS = --extern getopt=build/o/libgetopt.rlib \
--extern sysexits=build/o/libsysexits.rlib \
--extern strerror=build/o/libstrerror.rlib
CFLAGS += -I$(SYSEXITS)
.PHONY: all
all: dj false fop hru intcmp mm rpn scrut str strcmp swab true
all: dj false fop hru intcmp mm npc rpn scrut str strcmp swab true
build:
# keep build/include until bindgen(1) has stdin support
@@ -26,36 +37,40 @@ build:
.PHONY: clean
clean:
rm -rf build/ dist/
rm -rf build dist
dist: all
mkdir -p dist/bin dist/share/man/man1
cp build/bin/* dist/bin/
cp docs/*.1 dist/share/man/man1/
mkdir -p $(DESTDIR)/$(PREFIX)/bin $(DESTDIR)/$(PREFIX)/share/man/man1
cp build/bin/* $(DESTDIR)/$(PREFIX)/bin
cp docs/*.1 $(DESTDIR)/$(PREFIX)/share/man/man1
.PHONY: install
install: dist
mkdir -p $(PREFIX)
cp -r dist/* $(PREFIX)/
cp -r $(DESTDIR)/* /
.PHONY: test
test: build
tests/posix-compat.sh
$(RUSTC) --test src/getopt-rs/lib.rs -o build/test/getopt
build/o/libsysexits.rlib: build
.PHONY: rustlibs
rustlibs: build/o/libsysexits.rlib build/o/libgetopt.rlib \
build/o/libstrerror.rlib
build/o/libgetopt.rlib: build src/getopt-rs/lib.rs
$(RUSTC) $(RUSTFLAGS) --crate-type=lib --crate-name=getopt \
-o $@ src/getopt-rs/lib.rs
build/o/libstrerror.rlib: build src/strerror.rs
$(RUSTC) $(RUSTFLAGS) --crate-type=lib -o $@ \
src/strerror.rs
build/o/libsysexits.rlib: build $(SYSEXITS)sysexits.h
# bandage solution until bindgen(1) gets stdin support
printf '#define EXIT_FAILURE 1\n' | cat - include/sysexits.h \
printf '#define EXIT_FAILURE 1\n' | cat - $(SYSEXITS)sysexits.h \
> build/include/sysexits.h
bindgen --default-macro-constant-type signed --use-core --formatter=none \
"$$(printf '#include <sysexits.h>\n' \
| cpp -M -idirafter "build/include" - \
| sed 's/ /\n/g' | grep sysexits.h)" \
| $(RUSTC) $(RUSTFLAGS) --crate-type lib -o build/o/libsysexits.rlib -
build/o/libgetopt.rlib: src/getopt-rs/lib.rs
$(RUSTC) $(RUSTFLAGS) --crate-type=lib --crate-name=getopt \
-o build/o/libgetopt.rlib src/getopt-rs/lib.rs
build/include/sysexits.h | $(RUSTC) $(RUSTFLAGS) --crate-type lib -o $@ -
.PHONY: dj
dj: build/bin/dj
@@ -69,17 +84,13 @@ build/bin/false: src/false.c build
.PHONY: fop
fop: build/bin/fop
build/bin/fop: src/fop.rs build build/o/libgetopt.rlib build/o/libsysexits.rlib
$(RUSTC) $(RUSTFLAGS) --extern getopt=build/o/libgetopt.rlib \
--extern sysexits=build/o/libsysexits.rlib \
-o $@ src/fop.rs
build/bin/fop: src/fop.rs build rustlibs
$(RUSTC) $(RUSTFLAGS) $(RUSTLIBS) -o $@ src/fop.rs
.PHONY: hru
hru: build/bin/hru
build/bin/hru: src/hru.rs build build/o/libgetopt.rlib build/o/libsysexits.rlib
$(RUSTC) $(RUSTFLAGS) --extern getopt=build/o/libgetopt.rlib \
--extern sysexits=build/o/libsysexits.rlib \
-o $@ src/hru.rs
build/bin/hru: src/hru.rs build rustlibs
$(RUSTC) $(RUSTFLAGS) $(RUSTLIBS) -o $@ src/hru.rs
.PHONY: intcmp
intcmp: build/bin/intcmp
@@ -91,12 +102,20 @@ mm: build/bin/mm
build/bin/mm: src/mm.c build
$(CC) $(CFLAGS) -o $@ src/mm.c
.PHONY: npc
npc: build/bin/npc
build/bin/npc: src/npc.c build
$(CC) $(CFLAGS) -o $@ src/npc.c
.PHONY: pg
pg: build/bin/pg
build/bin/pg: src/pg.c build
$(CC) $(CFLAGS) -o $@ src/pg.c
.PHONY: rpn
rpn: build/bin/rpn
build/bin/rpn: src/rpn.rs build build/o/libsysexits.rlib
$(RUSTC) $(RUSTFLAGS) \
--extern sysexits=build/o/libsysexits.rlib \
-o $@ src/rpn.rs
build/bin/rpn: src/rpn.rs build rustlibs
$(RUSTC) $(RUSTFLAGS) $(RUSTLIBS) -o $@ src/rpn.rs
.PHONY: scrut
scrut: build/bin/scrut

41
docs/pg.1 Normal file
View File

@@ -0,0 +1,41 @@
.\" Copyright (c) 2024 DTB <trinity@trinity.moe>
.\"
.\" This work is licensed under CC BY-SA 4.0. To see a copy of this license,
.\" visit <http://creativecommons.org/licenses/by-sa/4.0/>.
.TH PG 1
.SH NAME
pg \(en paginate
.SH SYNOPSIS
pg
.RB ( -p
.RB [ prompt ])
.SH DESCRIPTION
Pg prints standard input to standard output, accepting commands between pages.
.SH OPTIONS
.B -p
.RS
Replaces the default prompt (": ") with the next argument.
.RE
.SH DIAGNOSTICS
Pg returns an unsuccessful exit code if the tty couldn't be opened or if pg was
invoked incorrectly (with any arguments).
.SH RATIONALE
Plan 9 from Bell Labs had p(1), a similar "cooked"-mode paginator (as opposed
to "raw" mode, which a vast majority of paginators use).
.SH SEE ALSO
more(1)

View File

@@ -203,28 +203,28 @@ Io_fdseek(struct Io *io){
if(!fdisstd(io->fd) && lseek(io->fd, io->seek, SEEK_SET) != -1)
return -1;
else if(io->fl == write_flags){
memset(io->buf, '\0', io->bs);
/* This is a dirty trick; rather than testing conditions and operating
* likewise, because the parameters to read or write are going to be
* the same either way, just use a function pointer to keep track of
* the intended operation. */
op = (int (*)(int, void *, size_t))&write;
/* Function pointer casts are risky; this works because the difference
* is in the second parameter and only that write(2) makes the buffer
* const whereas read(2) does not. To avoid even the slightest
* undefined behavior comment out the cast, just be ready for a
* -Wincompatible-function-pointer-types if your compiler notices it.
*/
}else
op = &read;
/* We're going to cheat and use bufuse as the retval for write(2), which is
* fine because it'll be zeroed as this function returns anyway. */
do{ if( (io->bufuse = (*op)(io->fd, io->buf, MIN(io->bs, io->seek))) == 0)
/* second chance */
io->bufuse = (*op)(io->fd, io->buf, MIN(io->bs, io->seek));
}while((io->seek -= io->bufuse) > 0 && io->bufuse != 0);
/* repeated code to get the condition out of the loop */
if(io->fl == write_flags){
memset(io->buf, '\0', io->bs);
/* We're going to cheat and use bufuse as the retval for write(2),
* which is fine because it'll be zeroed as this function returns
* anyway. */
do{
if((io->bufuse = write(io->fd, io->buf, MIN(io->bs, io->seek)))
== 0)
/* second chance */
io->bufuse = write(io->fd, io->buf, MIN(io->bs, io->seek));
}while((io->seek -= io->bufuse) > 0 && io->bufuse != 0);
}else if(io->fl == read_flags){
do{
if((io->bufuse = read(io->fd, io->buf, MIN(io->bs, io->seek)))
== 0)
/* second chance */
io->bufuse = read(io->fd, io->buf, MIN(io->bs, io->seek));
}while((io->seek -= io->bufuse) > 0 && io->bufuse != 0);
}else
return EX_SOFTWARE;
io->bufuse = 0;

View File

@@ -22,10 +22,12 @@ use std::{
process::{ Command, exit, Stdio },
};
extern crate sysexits;
extern crate getopt;
extern crate strerror;
extern crate sysexits;
use getopt::{ Opt, Parser };
use strerror::StrError;
use sysexits::{ EX_DATAERR, EX_IOERR, EX_UNAVAILABLE, EX_USAGE };
fn main() {
@@ -55,7 +57,7 @@ fn main() {
});
let index = argv[index_arg].parse::<usize>().unwrap_or_else(|e| {
eprintln!("{}: {}: {}.", argv[0], argv[1], e);
eprintln!("{}: {}: {}", argv[0], argv[1], e);
exit(EX_DATAERR);
});
@@ -75,13 +77,13 @@ fn main() {
.stdout(Stdio::piped())
.spawn()
.unwrap_or_else( |e| {
eprintln!("{}: {}: {}.", argv[0], argv[command_arg], e);
eprintln!("{}: {}: {}", argv[0], argv[command_arg], e.strerror());
exit(EX_UNAVAILABLE);
});
let field = fields.get(index).unwrap_or_else(|| {
eprintln!(
"{}: {}: No such index in input.",
"{}: {}: No such index in input",
argv[0],
index.to_string(),
);
@@ -94,7 +96,7 @@ fn main() {
}
let output = spawned.wait_with_output().unwrap_or_else(|e| {
eprintln!("{}: {}: {}.", argv[0], argv[command_arg], e);
eprintln!("{}: {}: {}", argv[0], argv[command_arg], e.strerror());
exit(EX_IOERR);
});
@@ -103,7 +105,7 @@ fn main() {
if replace.pop() != Some(b'\n') { replace = output.stdout; }
let new_field = String::from_utf8(replace).unwrap_or_else(|e| {
eprintln!("{}: {}: {}.", argv[0], argv[command_arg], e);
eprintln!("{}: {}: {}", argv[0], argv[command_arg], e);
exit(EX_IOERR);
});
@@ -111,8 +113,8 @@ fn main() {
stdout().write_all(
fields.join(&d.to_string()).as_bytes()
).unwrap_or_else(|e|{
eprintln!("{}: {}.", argv[0], e);
).unwrap_or_else(|e| {
eprintln!("{}: {}", argv[0], e.strerror());
exit(EX_IOERR);
});
}

View File

@@ -23,8 +23,10 @@ use std::{
process::{ ExitCode, exit },
};
extern crate strerror;
extern crate sysexits;
use strerror::StrError;
use sysexits::{ EX_DATAERR, EX_IOERR, EX_SOFTWARE };
const LIST: [(u32, &str); 10] = [
@@ -49,7 +51,7 @@ fn convert(input: u128) -> Result<(f64, (u32, &'static str)), String> {
let c = match 10_u128.checked_pow(n) {
Some(c) => c,
None => {
return Err(format!("10^{}: Integer overflow.", n.to_string()));
return Err(format!("10^{}: Integer overflow", n.to_string()));
},
};
@@ -79,7 +81,7 @@ fn main() -> ExitCode {
f
},
Err(err) => {
eprintln!("{}: {}.", argv[0], err);
eprintln!("{}: {}", argv[0], err);
return ExitCode::from(EX_DATAERR as u8);
},
};
@@ -87,7 +89,7 @@ fn main() -> ExitCode {
let (number, prefix) = match convert(n) {
Ok(x) => x,
Err(err) => {
eprintln!("{}: {}.", argv[0], err);
eprintln!("{}: {}", argv[0], err);
return ExitCode::from(EX_SOFTWARE as u8);
},
};
@@ -98,7 +100,7 @@ fn main() -> ExitCode {
stdout().write_all(format!("{} {}\n", out, si_prefix).as_bytes())
.unwrap_or_else(|e| {
eprintln!("{}: {}.", argv[0], e);
eprintln!("{}: {}", argv[0], e.strerror());
exit(EX_IOERR);
});
}

260
src/pg.c Normal file
View File

@@ -0,0 +1,260 @@
/*
* Copyright (c) 2024 DTB <trinity@trinity.moe>
* SPDX-License-Identifier: AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU Affero General Public License as published by the Free
* Software Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
* details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see https://www.gnu.org/licenses/.
*/
#include <errno.h> /* errno */
#include <stdio.h> /* fclose(3), feof(3), fgetc(3), fgets(3), fopen(3),
* fprintf(3), fputc(3), stderr, stdin, stdout, EOF, FILE,
* NULL */
#include <stdlib.h> /* size_t */
#include <string.h> /* strchr(3), strcmp(3), strerror(3), strtok(3) */
#include <unistd.h> /* getopt(3) */
/* Commands start with cmd_. They take an argc and NULL-terminated argv, like
* main, and return a status from <sysexits.h>. Return values other than EX_OK
* and EX_USAGE cause pg(1) to exit with that value, except EX_UNAVAILABLE,
* which causes pg(1) to exit with the status EX_OK. */
#include <sysexits.h>
#define CMDLINE_MAX 99+1 /* Maximum length of command line. */
static char *whitespace = " \n\r\t\v";
static struct {
size_t quantity;
enum { LINES = 0, BYTES = 1 } type;
} default_page_unit = { 22, LINES } /* Plan 9 default */;
static FILE *input;
static char *prompt = ": ";
static char *program_name = "pg";
static char *permute_out(char *str, size_t index){
size_t j;
for(j = index; str[j - 1] != '\0'; ++j)
str[j - 1] = str[j];
return str;
}
/* strtok(3p), but supports double-quotes and escapes (but only for escaping
* quotes). UTF-8 is safe only in str. Unmatched quotes in str are considered
* literal. The behavior of strtok_quoted when '"' or '\\' are in sep is
* undefined. */
/* TODO: Seems to only ever return NULL. */
static char *strtok_quoted(char *str, char *sep){
static char *s;
size_t i;
size_t j;
if(str != NULL)
s = str;
while(strchr(sep, *s) == NULL)
if(*++s == '\0')
return NULL; /* no remaining characters except seperators */
{
char in_escape;
int in_quotes;
in_escape = 0;
in_quotes = -1;
for(i = 0; s[i] != '\0'; ++i)
switch(s[i]){
case '\\':
if(in_escape)
permute_out(s, i--);
in_escape = !in_escape;
break;
case '"':
if(in_escape){
s[i] = s[i - 1];
permute_out(s, i--);
}else if(in_quotes != -1){
permute_out(s, in_quotes);
--i;
permute_out(s, i--);
in_quotes = -1;
}else
in_quotes = i;
break;
case '\0':
return s;
default:
if(!in_escape && strchr(sep, s[i]) != NULL){
s[i] = '\0';
return s;
}
}
}
return s;
}
/* Page at most l bytes from f without consideration of a buffer (print them to
* stdout). */
static int pg_b_u(FILE *f, size_t l){
int c;
while((c = fgetc(f)) != EOF)
if((c = fputc(c, stdout)) == EOF || --l == 0)
break;
return c;
}
/* Page at most l lines, which are lengths of characters terminated by nl, from
* f without consideration of a buffer (print them to stdout). */
static int pg_l_u(FILE *f, size_t l, char nl){
int c;
while((c = fgetc(f)) != EOF)
if((c = fputc(c, stdout)) == EOF || (l -= (c == nl)) == 0)
break;
return c;
}
static int cmd_quit(int argc, char **argv, char **envp){return EX_UNAVAILABLE;}
static int cmd_default_page(int argc, char **argv, char **envp){
if(argc > 1) /* This shouldn't be possible. */
return EX_USAGE;
if(default_page_unit.type == BYTES)
return pg_b_u(input, default_page_unit.quantity) == EOF
? EX_UNAVAILABLE
: EX_OK;
else if(default_page_unit.type == LINES)
return pg_l_u(input, default_page_unit.quantity, '\n') == EOF
? EX_UNAVAILABLE
: EX_OK;
else
return EX_SOFTWARE;
}
static int cmd_page_down_lines(int argc, char **argv, char **envp){
switch(argc){
case 1:
return cmd_default_page(argc, argv, envp);
case 2: /* not implemented */
default:
fprintf(stderr, "Usage: %s" /*" (lines)"*/ "\n", argv[0]);
return EX_USAGE;
}
}
/* A CmdMap must be NULL-terminated. */
static struct CmdMap{ char *name; int (*fn)(int, char **, char **); }
builtins[] = {
{ "", cmd_default_page },
{ "+", cmd_page_down_lines },
/* don't make the user feel trapped */
{ ":q", cmd_quit }, { ":q!", cmd_quit },
{ "exit", cmd_quit }, { "q", cmd_quit }, { "Q", cmd_quit },
{ "quit", cmd_quit }, { "ZZ", cmd_quit },
{ NULL, NULL }
};
#define ARGV_MAX 10
/* Find and execute the command in the command map, given a corresponding
* command line. */
static int cmdline_exec(struct CmdMap *map, char *cmdline, char **envp){
/* Command line word splitting is naive and based on whitespace ONLY; no
* fancy quoting or escaping here. Adding that would (ideally) entail
* replacing strtok(3) with something specific to this task. */
static int argc;
static char *argv[ARGV_MAX];
if((argv[(argc = 0)] = strtok(cmdline, whitespace)) == NULL){
while(cmdline[0] != '\0')
cmdline = &cmdline[1];
argv[argc] = cmdline;
argv[++argc] = NULL;
}else{
while((argv[++argc] = strtok(NULL, whitespace)) != NULL
&& argc < ARGV_MAX);
}
for(; map->name != NULL; map = &map[1])
if(strcmp(map->name, argv[0]) == 0)
return map->fn(argc, argv, envp);
fprintf(stderr, "%s: %s: not found\n", program_name, argv[0]);
return EX_USAGE;
}
int usage(char *s){
fprintf(stderr, "Usage: %s (-p [prompt])\n", s);
return EX_USAGE;
}
int main(int argc, char *argv[]){
unsigned char cmd[CMDLINE_MAX];
FILE *t;
input = stdin;
if(argc < 1)
program_name = argv[0];
else{
int c;
while((c = getopt(argc, argv, "p:")) != -1)
switch(c){
case 'p': prompt = optarg; break;
default:
return usage(program_name);
}
}
if(argc > optind)
return usage(program_name);
if((t = fopen("/dev/tty", "rb")) == NULL){
fprintf(stderr, "%s: /dev/tty: %s\n", program_name, strerror(errno));
return EX_OSERR;
}
for(;;){
fputs(prompt, stderr);
fgets((char *)cmd, (sizeof cmd) / (sizeof *cmd), t);
if(strchr((char *)cmd, '\n') == NULL){ /* fast-forward stream */
int c;
while((c = fgetc(t)) != '\n')
if(c == EOF)
break;
}
if(feof(t))
return EX_OK;
{ int r;
switch((r = cmdline_exec(builtins, (char *)cmd, NULL))){
case EX_OK: case EX_USAGE: break;
case EX_UNAVAILABLE: return EX_OK;
default: return r;
} }
}
/* UNREACHABLE */
return EX_SOFTWARE;
}

View File

@@ -172,7 +172,7 @@ fn eval(
};
} else {
return Err(EvaluationError {
message: format!("{}: Unexpected operation.", op),
message: format!("{}: Unexpected operation", op),
code: EX_DATAERR,
})
}

31
src/strerror.rs Normal file
View File

@@ -0,0 +1,31 @@
/*
* Copyright (c) 2024 Emma Tebibyte <emma@tebibyte.media>
* SPDX-License-Identifier: FSFAP
*
* Copying and distribution of this file, with or without modification, are
* permitted in any medium without royalty provided the copyright notice and
* this notice are preserved. This file is offered as-is, without any warranty.
*/
use std::ffi::{ c_int, c_char, CStr };
pub trait StrError { fn strerror(&self) -> String; }
impl StrError for std::io::Error {
/* wrapper function for use in Rust */
fn strerror(&self) -> String {
/* Get the raw OS error. If its None, what the hell is going on‽ */
let errno = self.raw_os_error().unwrap_or(0) as c_int;
/* Get a CStr from the error message so that its referenced and then
* convert it to an owned value. If the string is not valid UTF-8,
* return that error instead. */
match unsafe { CStr::from_ptr(strerror(errno)) }.to_str() {
Ok(s) => s.to_owned(), // yay!! :D
Err(e) => e.to_string(), // awww :(
}
}
}
/* binding to strerror(3p) */
extern "C" { fn strerror(errnum: c_int) -> *mut c_char; }

10
src/test.rs Normal file
View File

@@ -0,0 +1,10 @@
extern crate strerror;
use strerror::raw_message;
fn main() {
stdout.write_all(b"meow\n").unwrap_or_else(|e| {
eprintln!("{}", raw_message(e));
std::process::exit(1);
});
}