1 | # spec/ysh-methods
|
2 |
|
3 | ## our_shell: ysh
|
4 | ## oils_failures_allowed: 2
|
5 |
|
6 | #### => operator for pure computation is allowed (may be mandatory later)
|
7 |
|
8 | # later we may make it mandatory
|
9 |
|
10 | if ("abc" => startsWith("a")) {
|
11 | echo yes
|
12 | }
|
13 |
|
14 | var mylist = [1, 2, 3]
|
15 |
|
16 | # This one should be ->
|
17 | call mylist->pop()
|
18 | echo 'ok'
|
19 |
|
20 | ## STDOUT:
|
21 | yes
|
22 | ok
|
23 | ## END
|
24 |
|
25 | #### => can be used to chain free functions
|
26 |
|
27 | func dictfunc() {
|
28 | return ({k1: 'spam', k2: 'eggs'})
|
29 | }
|
30 |
|
31 | echo $[list(dictfunc()) => join('/') => upper()]
|
32 |
|
33 | # This is nicer and more consistent
|
34 | echo $[dictfunc() => list() => join('/') => upper()]
|
35 |
|
36 | ## STDOUT:
|
37 | K1/K2
|
38 | K1/K2
|
39 | ## END
|
40 |
|
41 | #### Str => startsWith(Str) and endsWith(Str), simple
|
42 | func test(s, p) { echo $[s => startsWith(p)] $[s => endsWith(p)] }
|
43 |
|
44 | call test('', '')
|
45 | call test('abc', '')
|
46 | call test('abc', 'a')
|
47 | call test('abc', 'b')
|
48 | call test('abc', 'c')
|
49 | call test('abc', 'z')
|
50 | call test('', 'abc')
|
51 | ## status: 0
|
52 | ## STDOUT:
|
53 | true true
|
54 | true true
|
55 | true false
|
56 | false false
|
57 | false true
|
58 | false false
|
59 | false false
|
60 | ## END
|
61 |
|
62 | #### Str => startsWith(Str) and endsWith(Str), matches bytes not runes
|
63 | func test(s, p) { echo $[s => startsWith(p)] $[s => endsWith(p)] }
|
64 |
|
65 | call test(b'\yce\ya3', u'\u{03a3}')
|
66 | call test(b'\yce\ya3', b'\yce')
|
67 | call test(b'\yce\ya3', b'\ya3')
|
68 | call test(b'\yce', b'\yce')
|
69 | ## status: 0
|
70 | ## STDOUT:
|
71 | true true
|
72 | true false
|
73 | false true
|
74 | true true
|
75 | ## END
|
76 |
|
77 | #### Str => startsWith(Str) and endsWith(Str), eggex
|
78 | func test(s, p) { echo $[s => startsWith(p)] $[s => endsWith(p)] }
|
79 |
|
80 | call test('abc', / d+ /)
|
81 | call test('abc', / [ a b c ] /)
|
82 | call test('abc', / 'abc' /)
|
83 | call test('cba', / d+ /)
|
84 | call test('cba', / [ a b c ] /)
|
85 | call test('cba', / 'abc' /)
|
86 | ## status: 0
|
87 | ## STDOUT:
|
88 | false false
|
89 | true true
|
90 | true true
|
91 | false false
|
92 | true true
|
93 | false false
|
94 | ## END
|
95 |
|
96 | #### Str => startsWith(Str) and endsWith(Str), eggex with anchors
|
97 | func test(s, p) { echo $[s => startsWith(p)] $[s => endsWith(p)] }
|
98 |
|
99 | call test('ab', / %start 'a' /)
|
100 | call test('ab', / 'a' %end /)
|
101 | call test('ab', / %start 'a' %end /)
|
102 | call test('ab', / %start 'b' /)
|
103 | call test('ab', / 'b' %end /)
|
104 | call test('ab', / %start 'b' %end /)
|
105 | ## status: 0
|
106 | ## STDOUT:
|
107 | true false
|
108 | false false
|
109 | false false
|
110 | false false
|
111 | false true
|
112 | false false
|
113 | ## END
|
114 |
|
115 | #### Str => startsWith(Str) and endsWith(Str), eggex matches bytes not runes
|
116 | func test(s, p) { echo $[s => startsWith(p)] $[s => endsWith(p)] }
|
117 |
|
118 | call test(u'\u{03a3}', / dot /)
|
119 | call test(u'\u{03a3}', / ![z] /)
|
120 | call test(b'\yce', / dot /) # Fails: eggex does not match bytes
|
121 | call test(b'\yce', / ![z] /) # Fails: eggex does not match bytes
|
122 | ## status: 0
|
123 | ## STDOUT:
|
124 | true true
|
125 | true true
|
126 | true true
|
127 | true true
|
128 | ## END
|
129 |
|
130 | #### Str => startsWith(), no args
|
131 | = 'abc' => startsWith()
|
132 | ## status: 3
|
133 |
|
134 | #### Str => startsWith(), too many args
|
135 | = 'abc' => startsWith('extra', 'arg')
|
136 | ## status: 3
|
137 |
|
138 | #### Str => endsWith(), no args
|
139 | = 'abc' => endsWith()
|
140 | ## status: 3
|
141 |
|
142 | #### Str => endsWith(), too many args
|
143 | = 'abc' => endsWith('extra', 'arg')
|
144 | ## status: 3
|
145 |
|
146 | #### Str => trim*() with no args trims whitespace
|
147 | func test(s) { write --sep ', ' --j8 $[s => trimStart()] $[s => trimEnd()] $[s => trim()] }
|
148 |
|
149 | call test("")
|
150 | call test(" ")
|
151 | call test("mystr")
|
152 | call test(" mystr")
|
153 | call test("mystr ")
|
154 | call test(" mystr ")
|
155 | call test(" my str ")
|
156 | ## status: 0
|
157 | ## STDOUT:
|
158 | "", "", ""
|
159 | "", "", ""
|
160 | "mystr", "mystr", "mystr"
|
161 | "mystr", " mystr", "mystr"
|
162 | "mystr ", "mystr", "mystr"
|
163 | "mystr ", " mystr", "mystr"
|
164 | "my str ", " my str", "my str"
|
165 | ## END
|
166 |
|
167 | #### Str => trim*() with a simple string pattern trims pattern
|
168 | func test(s, p) { write --sep ', ' --j8 $[s => trimStart(p)] $[s => trimEnd(p)] $[s => trim(p)] }
|
169 |
|
170 | call test('' , 'xyz')
|
171 | call test(' ' , 'xyz')
|
172 | call test('xy' , 'xyz')
|
173 | call test('yz' , 'xyz')
|
174 | call test('xyz' , 'xyz')
|
175 | call test('xyzxyz' , 'xyz')
|
176 | call test('xyzxyzxyz', 'xyz')
|
177 | ## status: 0
|
178 | ## STDOUT:
|
179 | "", "", ""
|
180 | " ", " ", " "
|
181 | "xy", "xy", "xy"
|
182 | "yz", "yz", "yz"
|
183 | "", "", ""
|
184 | "xyz", "xyz", ""
|
185 | "xyzxyz", "xyzxyz", "xyz"
|
186 | ## END
|
187 |
|
188 | #### Str => trim*() with a string pattern trims bytes not runes
|
189 | func test(s, p) { write --sep ', ' --j8 $[s => trimStart(p)] $[s => trimEnd(p)] $[s => trim(p)] }
|
190 |
|
191 | call test(b'\yce\ya3', u'\u{03a3}')
|
192 | call test(b'\yce\ya3', b'\yce')
|
193 | call test(b'\yce\ya3', b'\ya3')
|
194 | ## status: 0
|
195 | ## STDOUT:
|
196 | "", "", ""
|
197 | b'\ya3', "Σ", b'\ya3'
|
198 | "Σ", b'\yce', b'\yce'
|
199 | ## END
|
200 |
|
201 | #### Str => trim*() with an eggex pattern trims pattern
|
202 | func test(s, p) { write --sep ', ' --j8 $[s => trimStart(p)] $[s => trimEnd(p)] $[s => trim(p)] }
|
203 |
|
204 | call test('' , / 'xyz' /)
|
205 | call test(' ' , / 'xyz' /)
|
206 | call test('xy' , / 'xyz' /)
|
207 | call test('yz' , / 'xyz' /)
|
208 | call test('xyz' , / 'xyz' /)
|
209 | call test('xyzxyz' , / 'xyz' /)
|
210 | call test('xyzxyzxyz', / 'xyz' /)
|
211 | call test('xyzabcxyz', / 'xyz' /)
|
212 | call test('xyzabcxyz', / %start 'xyz' /)
|
213 | call test('xyzabcxyz', / 'xyz' %end /)
|
214 | call test('123abc123', / d+ /)
|
215 | ## status: 0
|
216 | ## STDOUT:
|
217 | "", "", ""
|
218 | " ", " ", " "
|
219 | "xy", "xy", "xy"
|
220 | "yz", "yz", "yz"
|
221 | "", "", ""
|
222 | "xyz", "xyz", ""
|
223 | "xyzxyz", "xyzxyz", "xyz"
|
224 | "abcxyz", "xyzabc", "abc"
|
225 | "abcxyz", "xyzabcxyz", "abcxyz"
|
226 | "xyzabcxyz", "xyzabc", "xyzabc"
|
227 | "abc123", "123abc", "abc"
|
228 | ## END
|
229 |
|
230 | #### Str => trim*() with an eggex pattern trims bytes not runes
|
231 | func test(s, p) { write --sep ', ' --j8 $[s => trimStart(p)] $[s => trimEnd(p)] $[s => trim(p)] }
|
232 |
|
233 | call test(u'\u{03a3}', / dot /) # Fails: eggex does not match bytes, so entire rune is trimmed.
|
234 | call test(u'\u{03a3}', / ![z] /) # Fails: eggex does not match bytes, so entire rune is trimmed.
|
235 | call test(b'\yce', / dot /) # Fails: eggex does not match bytes, so nothing is trimmed.
|
236 | call test(b'\yce', / ![z] /) # Fails: eggex does not match bytes, so nothing is trimmed.
|
237 | ## status: 0
|
238 | ## STDOUT:
|
239 | b'\ya3', b'\yce', ""
|
240 | b'\ya3', b'\yce', ""
|
241 | "", "", ""
|
242 | "", "", ""
|
243 | ## END
|
244 |
|
245 | #### Str => trim(), too many args
|
246 | = 'mystr' => trim('extra', 'args')
|
247 | ## status: 3
|
248 |
|
249 | #### Str => trimStart(), too many args
|
250 | = 'mystr' => trimStart('extra', 'args')
|
251 | ## status: 3
|
252 |
|
253 | #### Str => trimEnd(), too many args
|
254 | = 'mystr' => trimEnd('extra', 'args')
|
255 | ## status: 3
|
256 |
|
257 | #### Str => trim(), unicode whitespace aware
|
258 |
|
259 | # Supported set of whitespace characters. The full set of Unicode whitespace
|
260 | # characters is not supported. See comments in the implementation.
|
261 | var spaces = [
|
262 | b'\u{0009}', # Horizontal tab (\t)
|
263 | b'\u{000A}', # Newline (\n)
|
264 | b'\u{000B}', # Vertical tab (\v)
|
265 | b'\u{000C}', # Form feed (\f)
|
266 | b'\u{000D}', # Carriage return (\r)
|
267 | b'\u{0020}', # Normal space
|
268 | b'\u{00A0}', # No-break space <NBSP>
|
269 | b'\u{FEFF}', # Zero-width no-break space <ZWNBSP>
|
270 | ] => join('')
|
271 |
|
272 | echo $["$spaces YSH $spaces" => trim()]
|
273 | ## status: 0
|
274 | ## STDOUT:
|
275 | YSH
|
276 | ## END
|
277 |
|
278 | #### Str => trim*(), unicode decoding errors
|
279 | var badUtf = b'\yF9'
|
280 |
|
281 | echo trim
|
282 |
|
283 | # We only decode UTF until the first non-space char. So the bad UTF-8 is
|
284 | # missed.
|
285 | try { call " a$[badUtf]b " => trim() }
|
286 | echo status=$_status
|
287 |
|
288 | # These require trim to decode the badUtf, so an error is raised
|
289 | try { call "$[badUtf]b " => trim() }
|
290 | echo status=$_status
|
291 | try { call " a$[badUtf]" => trim() }
|
292 | echo status=$_status
|
293 |
|
294 | # Similarly, trim{Left,Right} will assume correct encoding until shown
|
295 | # otherwise.
|
296 | echo trimStart
|
297 | try { call " a$[badUtf]" => trimStart() }
|
298 | echo status=$_status
|
299 | try { call "$[badUtf]b " => trimStart() }
|
300 | echo status=$_status
|
301 |
|
302 | echo trimEnd
|
303 | try { call "$[badUtf]b " => trimEnd() }
|
304 | echo status=$_status
|
305 | try { call " a$[badUtf]" => trimEnd() }
|
306 | echo status=$_status
|
307 |
|
308 | ## STDOUT:
|
309 | trim
|
310 | status=0
|
311 | status=3
|
312 | status=3
|
313 | trimStart
|
314 | status=0
|
315 | status=3
|
316 | trimEnd
|
317 | status=0
|
318 | status=3
|
319 | ## END
|
320 |
|
321 | #### Str => trimStart(), unicode decoding error types
|
322 | var badStrs = [
|
323 | b'\yF4\yA2\yA4\yB0', # Too large of a codepoint
|
324 | b'\yED\yBF\y80', # Surrogate
|
325 | b'\yC1\y81', # Overlong
|
326 | b'\y80', b'\yFF', # Does not match UTF8 bit pattern
|
327 | ]
|
328 |
|
329 | for badStr in (badStrs) {
|
330 | try { call badStr => trimStart() }
|
331 | echo status=$_status
|
332 | }
|
333 |
|
334 | ## STDOUT:
|
335 | status=3
|
336 | status=3
|
337 | status=3
|
338 | status=3
|
339 | status=3
|
340 | ## END
|
341 |
|
342 | #### Str => trimEnd(), unicode decoding error types
|
343 | # Tests the backwards UTF-8 decoder
|
344 | var badStrs = [
|
345 | b'\yF4\yA2\yA4\yB0', # Too large of a codepoint
|
346 | b'\yED\yBF\y80', # Surrogate
|
347 | b'\yC1\y81', # Overlong
|
348 | b'\y80', b'\yFF', # Does not match UTF8 bit pattern
|
349 | ]
|
350 |
|
351 | for badStr in (badStrs) {
|
352 | try { call badStr => trimEnd() }
|
353 | echo status=$_status
|
354 | }
|
355 |
|
356 | ## STDOUT:
|
357 | status=3
|
358 | status=3
|
359 | status=3
|
360 | status=3
|
361 | status=3
|
362 | ## END
|
363 |
|
364 | #### Str => trim*(), zero-codepoints are not NUL-terminators
|
365 | json write (b' \y00 ' => trim())
|
366 | json write (b' \y00 ' => trimStart())
|
367 | json write (b' \y00 ' => trimEnd())
|
368 | ## STDOUT:
|
369 | "\u0000"
|
370 | "\u0000 "
|
371 | " \u0000"
|
372 | ## END
|
373 |
|
374 | #### Dict => keys()
|
375 | var en2fr = {}
|
376 | setvar en2fr["hello"] = "bonjour"
|
377 | setvar en2fr["friend"] = "ami"
|
378 | setvar en2fr["cat"] = "chat"
|
379 | pp test_ (en2fr => keys())
|
380 | ## status: 0
|
381 | ## STDOUT:
|
382 | (List) ["hello","friend","cat"]
|
383 | ## END
|
384 |
|
385 | #### Str => split(sep), non-empty str sep
|
386 | pp test_ ('a,b,c'.split(','))
|
387 | pp test_ ('aa'.split('a'))
|
388 | pp test_ ('a<>b<>c<d'.split('<>'))
|
389 | pp test_ ('a;b;;c'.split(';'))
|
390 | pp test_ (''.split('foo'))
|
391 | ## STDOUT:
|
392 | (List) ["a","b","c"]
|
393 | (List) ["","",""]
|
394 | (List) ["a","b","c<d"]
|
395 | (List) ["a","b","","c"]
|
396 | (List) []
|
397 | ## END
|
398 |
|
399 | #### Str => split(sep), eggex sep
|
400 | pp test_ ('a,b;c'.split(/ ',' | ';' /))
|
401 | pp test_ ('aa'.split(/ dot /))
|
402 | pp test_ ('a<>b@@c<d'.split(/ '<>' | '@@' /))
|
403 | pp test_ ('a b cd'.split(/ space+ /))
|
404 | pp test_ (''.split(/ dot /))
|
405 | ## STDOUT:
|
406 | (List) ["a","b","c"]
|
407 | (List) ["","",""]
|
408 | (List) ["a","b","c<d"]
|
409 | (List) ["a","b","cd"]
|
410 | (List) []
|
411 | ## END
|
412 |
|
413 | #### Str => split(sep, count), non-empty str sep
|
414 | pp test_ ('a,b,c'.split(',', count=-1))
|
415 | pp test_ ('a,b,c'.split(',', count=-2)) # Any negative count means "ignore count"
|
416 | pp test_ ('aa'.split('a', count=1))
|
417 | pp test_ ('a<>b<>c<d'.split('<>', count=10))
|
418 | pp test_ ('a;b;;c'.split(';', count=2))
|
419 | pp test_ (''.split('foo', count=3))
|
420 | pp test_ ('a,b,c'.split(',', count=0))
|
421 | pp test_ (''.split(',', count=0))
|
422 | ## STDOUT:
|
423 | (List) ["a","b","c"]
|
424 | (List) ["a","b","c"]
|
425 | (List) ["","a"]
|
426 | (List) ["a","b","c<d"]
|
427 | (List) ["a","b",";c"]
|
428 | (List) []
|
429 | (List) ["a,b,c"]
|
430 | (List) []
|
431 | ## END
|
432 |
|
433 | #### Str => split(sep, count), eggex sep
|
434 | pp test_ ('a,b;c'.split(/ ',' | ';' /, count=-1))
|
435 | pp test_ ('aa'.split(/ dot /, count=1))
|
436 | pp test_ ('a<>b@@c<d'.split(/ '<>' | '@@' /, count=50))
|
437 | pp test_ ('a b c'.split(/ space+ /, count=0))
|
438 | pp test_ (''.split(/ dot /, count=1))
|
439 | ## STDOUT:
|
440 | (List) ["a","b","c"]
|
441 | (List) ["","a"]
|
442 | (List) ["a","b","c<d"]
|
443 | (List) ["a b c"]
|
444 | (List) []
|
445 | ## END
|
446 |
|
447 | #### Str => split(), usage errors
|
448 | try { pp test_ ('abc'.split('')) } # Sep cannot be ""
|
449 | echo status=$[_error.code]
|
450 | try { pp test_ ('abc'.split()) } # Sep must be present
|
451 | echo status=$[_error.code]
|
452 | try { pp test_ (b'\y00a\y01'.split(/ 'a' /)) } # Cannot split by eggex when str has NUL-byte
|
453 | echo status=$[_error.code]
|
454 | try { pp test_ (b'abc'.split(/ space* /)) } # Eggex cannot accept empty string
|
455 | echo status=$[_error.code]
|
456 | try { pp test_ (b'abc'.split(/ dot* /)) } # ...even without a zero-width match
|
457 | echo status=$[_error.code]
|
458 | ## STDOUT:
|
459 | status=3
|
460 | status=3
|
461 | status=3
|
462 | status=3
|
463 | status=3
|
464 | ## END
|
465 |
|
466 | #### Str => split(), non-ascii
|
467 | pp test_ ('🌞🌝🌞🌝🌞'.split('🌝'))
|
468 | pp test_ ('🌞🌝🌞🌝🌞'.split(/ '🌝' /))
|
469 | ## STDOUT:
|
470 | (List) ["🌞","🌞","🌞"]
|
471 | (List) ["🌞","🌞","🌞"]
|
472 | ## END
|
473 |
|
474 | #### Dict => values()
|
475 | var en2fr = {}
|
476 | setvar en2fr["hello"] = "bonjour"
|
477 | setvar en2fr["friend"] = "ami"
|
478 | setvar en2fr["cat"] = "chat"
|
479 | pp test_ (en2fr => values())
|
480 | ## status: 0
|
481 | ## STDOUT:
|
482 | (List) ["bonjour","ami","chat"]
|
483 | ## END
|
484 |
|
485 | #### Dict -> erase()
|
486 | var book = {title: "The Histories", author: "Herodotus"}
|
487 | call book->erase("author")
|
488 | pp test_ (book)
|
489 | # confirm method is idempotent
|
490 | call book->erase("author")
|
491 | pp test_ (book)
|
492 | ## status: 0
|
493 | ## STDOUT:
|
494 | (Dict) {"title":"The Histories"}
|
495 | (Dict) {"title":"The Histories"}
|
496 | ## END
|
497 |
|
498 | #### Dict -> get()
|
499 | var book = {title: "Hitchhiker's Guide", published: 1979}
|
500 | pp test_ (book => get("title", ""))
|
501 | pp test_ (book => get("published", 0))
|
502 | pp test_ (book => get("author", ""))
|
503 | ## status: 0
|
504 | ## STDOUT:
|
505 | (Str) "Hitchhiker's Guide"
|
506 | (Int) 1979
|
507 | (Str) ""
|
508 | ## END
|
509 |
|
510 | #### Separation of -> attr and () calling
|
511 | const check = "abc" => startsWith
|
512 | pp test_ (check("a"))
|
513 | ## status: 0
|
514 | ## STDOUT:
|
515 | (Bool) true
|
516 | ## END
|
517 |
|
518 | #### Bound methods, receiver value/reference semantics
|
519 | var is_a_ref = { "foo": "bar" }
|
520 | const f = is_a_ref => keys
|
521 | pp test_ (f())
|
522 | setvar is_a_ref["baz"] = 42
|
523 | pp test_ (f())
|
524 |
|
525 | var is_a_val = "abc"
|
526 | const g = is_a_val => startsWith
|
527 | pp test_ (g("a"))
|
528 | setvar is_a_val = "xyz"
|
529 | pp test_ (g("a"))
|
530 | ## status: 0
|
531 | ## STDOUT:
|
532 | (List) ["foo"]
|
533 | (List) ["foo","baz"]
|
534 | (Bool) true
|
535 | (Bool) true
|
536 | ## END
|
537 |
|
538 | #### List => indexOf()
|
539 | var items = [1, '2', 3, { 'a': 5 }]
|
540 |
|
541 | json write (items => indexOf('a'))
|
542 | json write (items => indexOf(1))
|
543 | json write (items => indexOf('2'))
|
544 | json write (items => indexOf({'a': 5}))
|
545 | ## STDOUT:
|
546 | -1
|
547 | 0
|
548 | 1
|
549 | 3
|
550 | ## END
|
551 |
|
552 | #### List => join()
|
553 | var items = [1, 2, 3]
|
554 |
|
555 | json write (items => join()) # default separator is ''
|
556 | json write (items => join(" ")) # explicit separator (can be any number or chars)
|
557 | json write (items => join(", ")) # separator can be any number of chars
|
558 |
|
559 | try {
|
560 | json write (items => join(1)) # separator must be a string
|
561 | }
|
562 | echo "failed with status $_status"
|
563 | ## STDOUT:
|
564 | "123"
|
565 | "1 2 3"
|
566 | "1, 2, 3"
|
567 | failed with status 3
|
568 | ## END
|
569 |
|
570 | #### List->reverse()
|
571 |
|
572 | var empty = []
|
573 |
|
574 | var a = [0]
|
575 | var b = [2, 1, 3]
|
576 | var c = :| hello world |
|
577 |
|
578 | call empty->reverse()
|
579 | call a->reverse()
|
580 | call b->reverse()
|
581 | call c->reverse()
|
582 |
|
583 | pp test_ (empty)
|
584 | pp test_ (a)
|
585 | pp test_ (b)
|
586 | pp test_ (c)
|
587 |
|
588 | ## STDOUT:
|
589 | (List) []
|
590 | (List) [0]
|
591 | (List) [3,1,2]
|
592 | (List) ["world","hello"]
|
593 | ## END
|
594 |
|
595 | #### List->reverse() from iterator
|
596 | var x = list(0 .. 3)
|
597 | call x->reverse()
|
598 | write @x
|
599 | ## STDOUT:
|
600 | 2
|
601 | 1
|
602 | 0
|
603 | ## END
|
604 |
|