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 -> clear()
556 var book = {title: "The Histories", author: "Herodotus"}
557 call book->clear()
558 pp test_ (book)
559 # confirm method is idempotent
560 call book->clear()
561 pp test_ (book)
562 ## status: 0
563 ## STDOUT:
564 (Dict) {}
565 (Dict) {}
566 ## END
567
568 #### Dict -> get()
569 var book = {title: "Hitchhiker's Guide", published: 1979}
570 pp test_ (book => get("title", ""))
571 pp test_ (book => get("published", 0))
572 pp test_ (book => get("author", ""))
573 ## status: 0
574 ## STDOUT:
575 (Str) "Hitchhiker's Guide"
576 (Int) 1979
577 (Str) ""
578 ## END
579
580 #### Dict -> add() with ints
581 var histogram = {a: 1979}
582 call histogram->add('a')
583 assert [{"a": 1980} === histogram]
584 call histogram->add('a', 3)
585 assert [{"a": 1983} === histogram]
586 call histogram->add('b')
587 assert [{"a": 1983, "b": 1} === histogram]
588 call histogram->add('c', 3)
589 assert [{"a": 1983, "b": 1, "c": 3} === histogram]
590 ## status: 0
591
592 #### Dict -> add() with floats
593 var histogram = {a: 2.5}
594 call histogram->add('a')
595 pp test_ (histogram)
596 call histogram->add('a', 2)
597 pp test_ (histogram)
598 call histogram->add('a', 3.5)
599 pp test_ (histogram)
600 call histogram->add('b', 3.5)
601 pp test_ (histogram)
602 call histogram->add('b', 2)
603 pp test_ (histogram)
604 call histogram->add('c', 1)
605 pp test_ (histogram)
606 call histogram->add('c', 3.0)
607 pp test_ (histogram)
608 ## status: 0
609 ## STDOUT:
610 (Dict) {"a":3.5}
611 (Dict) {"a":5.5}
612 (Dict) {"a":9.0}
613 (Dict) {"a":9.0,"b":3.5}
614 (Dict) {"a":9.0,"b":5.5}
615 (Dict) {"a":9.0,"b":5.5,"c":1}
616 (Dict) {"a":9.0,"b":5.5,"c":4.0}
617 ## END
618
619 #### Dict -> append()
620 var mydict = {a: [1]}
621 call mydict->append('a', 2)
622 assert [{"a": [1,2]} === mydict]
623 call mydict->append('a', 'b')
624 assert [{"a": [1,2,'b']} === mydict]
625 call mydict->append('b', 1)
626 assert [{"a": [1,2,'b'], "b": [1]} === mydict]
627 call mydict->append('c', [1,2])
628 # list of lists
629 assert [{"a": [1,2,'b'], "b": [1], "c": [[1,2]]} === mydict]
630 ## status: 0
631
632 #### Separation of -> attr and () calling
633 const check = "abc" => startsWith
634 pp test_ (check("a"))
635 ## status: 0
636 ## STDOUT:
637 (Bool) true
638 ## END
639
640 #### Bound methods, receiver value/reference semantics
641 var is_a_ref = { "foo": "bar" }
642 const f = is_a_ref => keys
643 pp test_ (f())
644 setvar is_a_ref["baz"] = 42
645 pp test_ (f())
646
647 var is_a_val = "abc"
648 const g = is_a_val => startsWith
649 pp test_ (g("a"))
650 setvar is_a_val = "xyz"
651 pp test_ (g("a"))
652 ## status: 0
653 ## STDOUT:
654 (List) ["foo"]
655 (List) ["foo","baz"]
656 (Bool) true
657 (Bool) true
658 ## END
659
660 #### List->clear()
661 var empty = []
662 var items = [1, 2, 3]
663
664 call empty->clear()
665 call items->clear()
666
667 pp test_ (empty)
668 pp test_ (items)
669
670 ## STDOUT:
671 (List) []
672 (List) []
673 ## END
674
675 #### List => indexOf()
676 var items = [1, '2', 3, { 'a': 5 }]
677
678 json write (items => indexOf('a'))
679 json write (items => indexOf(1))
680 json write (items => indexOf('2'))
681 json write (items => indexOf({'a': 5}))
682 ## STDOUT:
683 -1
684 0
685 1
686 3
687 ## END
688
689 #### List => lastIndexOf()
690 var items = ['a', 'b', 'c', 'b', 'a']
691
692 json write (items => lastIndexOf('a'))
693 json write (items => lastIndexOf('b'))
694 json write (items => lastIndexOf('c'))
695 json write (items => lastIndexOf('d'))
696 ## STDOUT:
697 4
698 3
699 2
700 -1
701 ## END
702
703 #### List => join()
704 var items = [1, 2, 3]
705
706 json write (items => join()) # default separator is ''
707 json write (items => join(" ")) # explicit separator (can be any number or chars)
708 json write (items => join(", ")) # separator can be any number of chars
709
710 try {
711 json write (items => join(1)) # separator must be a string
712 }
713 echo "failed with status $_status"
714 ## STDOUT:
715 "123"
716 "1 2 3"
717 "1, 2, 3"
718 failed with status 3
719 ## END
720
721 #### List->reverse()
722
723 var empty = []
724
725 var a = [0]
726 var b = [2, 1, 3]
727 var c = :| hello world |
728
729 call empty->reverse()
730 call a->reverse()
731 call b->reverse()
732 call c->reverse()
733
734 pp test_ (empty)
735 pp test_ (a)
736 pp test_ (b)
737 pp test_ (c)
738
739 ## STDOUT:
740 (List) []
741 (List) [0]
742 (List) [3,1,2]
743 (List) ["world","hello"]
744 ## END
745
746 #### List->reverse() from iterator
747 var x = list(0 ..< 3)
748 call x->reverse()
749 write @x
750 ## STDOUT:
751 2
752 1
753 0
754 ## END
755