1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
use super::*;
#[derive(Default)]
pub struct Repeat {
count: Option<usize>,
max: Option<usize>,
quiet: bool,
no_newline: bool,
}
impl StringSubCommand<'_> for Repeat {
const LONG_OPTIONS: &'static [WOption<'static>] = &[
wopt(L!("count"), RequiredArgument, 'n'),
wopt(L!("max"), RequiredArgument, 'm'),
wopt(L!("quiet"), NoArgument, 'q'),
wopt(L!("no-newline"), NoArgument, 'N'),
];
const SHORT_OPTIONS: &'static wstr = L!("n:m:qN");
fn parse_opt(&mut self, c: char, arg: Option<&wstr>) -> Result<(), StringError<'_>> {
match c {
'n' => {
let arg = arg.unwrap();
self.count = Some(
Self::parse_arg_number(arg)?
.try_into()
.map_err(|_| err_fmt!("Invalid count value '%s'", arg))?,
);
}
'm' => {
let arg = arg.unwrap();
self.max = Some(
Self::parse_arg_number(arg)?
.try_into()
.map_err(|_| err_fmt!(Error::INVALID_MAX_VALUE, arg))?,
);
}
'q' => self.quiet = true,
'N' => self.no_newline = true,
_ => return Err(StringError::UnknownOption),
}
Ok(())
}
fn take_args(
&mut self,
optind: &mut usize,
args: &[&'_ wstr],
streams: &mut IoStreams,
) -> Result<(), ErrorCode> {
if self.count.is_some() || self.max.is_some() {
return Ok(());
}
let cmd = L!("string");
let subcmd = args[0];
let Some(arg) = args.get(*optind) else {
err_fmt!(Error::MISSING_ARG)
.subcmd(cmd, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
};
*optind += 1;
let Ok(Ok(count)) = fish_wcstol(arg).map(|count| count.try_into()) else {
err_fmt!("Invalid count value '%s'", arg)
.subcmd(cmd, subcmd)
.finish(streams);
return Err(STATUS_INVALID_ARGS);
};
self.count = Some(count);
Ok(())
}
fn handle(
&mut self,
_parser: &Parser,
streams: &mut IoStreams,
optind: &mut usize,
args: &[&wstr],
) -> Result<(), ErrorCode> {
let max = self.max.unwrap_or_default();
let count = self.count.unwrap_or_default();
if max == 0 && count == 0 {
// XXX: This used to be allowed, but returned 1.
// Keep it that way for now instead of adding an error.
// streams.err.append(L"Count or max must be greater than zero");
return Err(STATUS_CMD_ERROR);
}
let mut all_empty = true;
let mut first = true;
let mut print_trailing_newline = true;
for InputValue { arg, want_newline } in arguments(args, optind, streams) {
print_trailing_newline = want_newline;
if arg.is_empty() {
continue;
}
all_empty = false;
if self.quiet {
// Early out if we can - see #7495.
return Ok(());
}
if !first {
streams.out.append('\n');
}
first = false;
// The maximum size of the string is either the "max" characters,
// or it's the "count" repetitions, whichever ends up lower.
let max_repeat_len = arg.len().wrapping_mul(count);
let max = if max == 0 || (count > 0 && max_repeat_len < max) {
// TODO: we should disallow overflowing unless max <= w.len().checked_mul(self.count).unwrap_or(usize::MAX)
max_repeat_len
} else {
max
};
// Reserve a string to avoid writing constantly.
// The 1500 here is a total gluteal extraction, but 500 seems to perform slightly worse.
let chunk_size = 1500;
// The + word length is so we don't have to hit the chunk size exactly,
// which would require us to restart in the middle of the string.
// E.g. imagine repeating "12345678". The first chunk is hit after a last "1234",
// so we would then have to restart by appending "5678", which requires a substring.
// So let's not bother.
//
// Unless of course we don't even print the entire word, in which case we just need max.
let mut chunk = WString::with_capacity(max.min(chunk_size + arg.len()));
let mut i = max;
while i > 0 {
if i >= arg.len() {
chunk.push_utfstr(&arg);
} else {
chunk.push_utfstr(arg.slice_to(i));
break;
}
i -= arg.len();
if chunk.len() >= chunk_size {
// We hit the chunk size, write it repeatedly until we can't anymore.
streams.out.append(&chunk);
while i >= chunk.len() {
streams.out.append(&chunk);
// We can easily be asked to write *a lot* of data,
// so we need to check every so often if the pipe has been closed.
// If we didn't, running `string repeat -n LARGENUMBER foo | pv`
// and pressing ctrl-c seems to hang.
if streams.out.flush_and_check_error() != STATUS_CMD_OK {
return Err(STATUS_CMD_ERROR);
}
i -= chunk.len();
}
chunk.clear();
}
}
// Flush the remainder.
if !chunk.is_empty() {
streams.out.append(&chunk);
}
}
// Historical behavior is to never append a newline if all strings were empty.
if !self.quiet && !self.no_newline && !all_empty && print_trailing_newline {
streams.out.append('\n');
}
if all_empty {
Err(STATUS_CMD_ERROR)
} else {
Ok(())
}
}
}