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 -> inc()
581 var histogram = {a: 2.5, b: 1979}
582 call histogram->inc('a')
583 pp test_ (histogram)
584 call histogram->inc('a', 3.5)
585 pp test_ (histogram)
586 call histogram->inc('b')
587 pp test_ (histogram)
588 call histogram->inc('b', 3)
589 pp test_ (histogram)
590 call histogram->inc('c', 3)
591 pp test_ (histogram)
592 call histogram->inc('d', 3.5)
593 pp test_ (histogram)
594 ## status: 0
595 ## STDOUT:
596 (Dict) {"a":3.5,"b":1979}
597 (Dict) {"a":7.0,"b":1979}
598 (Dict) {"a":7.0,"b":1980}
599 (Dict) {"a":7.0,"b":1983}
600 (Dict) {"a":7.0,"b":1983,"c":3}
601 (Dict) {"a":7.0,"b":1983,"c":3,"d":3.5}
602 ## END
603
604 #### Separation of -> attr and () calling
605 const check = "abc" => startsWith
606 pp test_ (check("a"))
607 ## status: 0
608 ## STDOUT:
609 (Bool) true
610 ## END
611
612 #### Bound methods, receiver value/reference semantics
613 var is_a_ref = { "foo": "bar" }
614 const f = is_a_ref => keys
615 pp test_ (f())
616 setvar is_a_ref["baz"] = 42
617 pp test_ (f())
618
619 var is_a_val = "abc"
620 const g = is_a_val => startsWith
621 pp test_ (g("a"))
622 setvar is_a_val = "xyz"
623 pp test_ (g("a"))
624 ## status: 0
625 ## STDOUT:
626 (List) ["foo"]
627 (List) ["foo","baz"]
628 (Bool) true
629 (Bool) true
630 ## END
631
632 #### List->clear()
633 var empty = []
634 var items = [1, 2, 3]
635
636 call empty->clear()
637 call items->clear()
638
639 pp test_ (empty)
640 pp test_ (items)
641
642 ## STDOUT:
643 (List) []
644 (List) []
645 ## END
646
647 #### List => indexOf()
648 var items = [1, '2', 3, { 'a': 5 }]
649
650 json write (items => indexOf('a'))
651 json write (items => indexOf(1))
652 json write (items => indexOf('2'))
653 json write (items => indexOf({'a': 5}))
654 ## STDOUT:
655 -1
656 0
657 1
658 3
659 ## END
660
661 #### List => lastIndexOf()
662 var items = ['a', 'b', 'c', 'b', 'a']
663
664 json write (items => lastIndexOf('a'))
665 json write (items => lastIndexOf('b'))
666 json write (items => lastIndexOf('c'))
667 json write (items => lastIndexOf('d'))
668 ## STDOUT:
669 4
670 3
671 2
672 -1
673 ## END
674
675 #### List => join()
676 var items = [1, 2, 3]
677
678 json write (items => join()) # default separator is ''
679 json write (items => join(" ")) # explicit separator (can be any number or chars)
680 json write (items => join(", ")) # separator can be any number of chars
681
682 try {
683 json write (items => join(1)) # separator must be a string
684 }
685 echo "failed with status $_status"
686 ## STDOUT:
687 "123"
688 "1 2 3"
689 "1, 2, 3"
690 failed with status 3
691 ## END
692
693 #### List->reverse()
694
695 var empty = []
696
697 var a = [0]
698 var b = [2, 1, 3]
699 var c = :| hello world |
700
701 call empty->reverse()
702 call a->reverse()
703 call b->reverse()
704 call c->reverse()
705
706 pp test_ (empty)
707 pp test_ (a)
708 pp test_ (b)
709 pp test_ (c)
710
711 ## STDOUT:
712 (List) []
713 (List) [0]
714 (List) [3,1,2]
715 (List) ["world","hello"]
716 ## END
717
718 #### List->reverse() from iterator
719 var x = list(0 ..< 3)
720 call x->reverse()
721 write @x
722 ## STDOUT:
723 2
724 1
725 0
726 ## END
727