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