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