init repo
This commit is contained in:
commit
0131d6d955
457
.cover
Normal file
457
.cover
Normal file
@ -0,0 +1,457 @@
|
||||
mode: set
|
||||
code.squareroundforest.org/arpio/wand/cmd/wand-docs/main.go:3.14,4.2 0 0
|
||||
code.squareroundforest.org/arpio/wand/apply.go:9.54,10.15 1 0
|
||||
code.squareroundforest.org/arpio/wand/apply.go:10.15,12.3 1 0
|
||||
code.squareroundforest.org/arpio/wand/apply.go:14.2,14.31 1 0
|
||||
code.squareroundforest.org/arpio/wand/apply.go:17.52,18.17 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:18.17,22.3 3 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:24.2,24.17 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:24.17,27.3 2 0
|
||||
code.squareroundforest.org/arpio/wand/apply.go:29.2,29.31 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:29.31,31.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:34.47,35.25 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:36.23,37.32 1 0
|
||||
code.squareroundforest.org/arpio/wand/apply.go:38.21,39.30 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:43.50,45.2 1 0
|
||||
code.squareroundforest.org/arpio/wand/apply.go:47.48,48.31 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:48.31,50.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:53.41,54.14 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:54.14,57.3 2 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:59.2,59.47 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:62.52,63.22 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:64.23,65.28 1 0
|
||||
code.squareroundforest.org/arpio/wand/apply.go:66.21,67.26 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:68.10,69.24 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:73.56,74.43 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:74.43,80.10 6 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:81.39,83.24 2 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:84.55,86.39 2 0
|
||||
code.squareroundforest.org/arpio/wand/apply.go:86.39,89.5 2 0
|
||||
code.squareroundforest.org/arpio/wand/apply.go:90.21,92.33 2 0
|
||||
code.squareroundforest.org/arpio/wand/apply.go:97.100,101.23 4 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:101.23,103.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:105.2,106.42 2 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:106.42,109.3 2 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:111.2,112.23 2 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:112.23,114.41 2 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:114.41,116.4 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:118.3,118.28 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:121.2,122.26 2 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:122.26,123.12 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:123.12,125.4 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:128.2,129.20 2 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:129.20,130.12 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:130.12,132.4 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:135.2,135.50 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:135.50,137.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:139.2,140.29 2 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:140.29,142.34 2 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:142.34,144.4 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:146.3,146.27 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:149.2,149.33 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:149.33,151.28 2 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:151.28,153.4 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:155.3,155.27 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:158.2,158.32 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:161.63,165.2 3 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:167.93,170.33 3 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:170.33,174.28 4 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:174.28,175.69 1 0
|
||||
code.squareroundforest.org/arpio/wand/apply.go:175.69,177.5 1 0
|
||||
code.squareroundforest.org/arpio/wand/apply.go:178.9,178.23 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:178.23,181.4 2 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:181.9,181.22 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:181.22,182.33 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:182.33,184.5 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:185.9,189.4 3 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:192.2,192.13 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:195.73,196.19 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:196.19,198.3 1 0
|
||||
code.squareroundforest.org/arpio/wand/apply.go:200.2,203.40 4 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:203.40,205.3 1 0
|
||||
code.squareroundforest.org/arpio/wand/apply.go:207.2,207.17 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:207.17,209.3 1 0
|
||||
code.squareroundforest.org/arpio/wand/apply.go:211.2,212.24 2 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:212.24,214.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:216.2,216.20 1 1
|
||||
code.squareroundforest.org/arpio/wand/apply.go:219.59,226.2 6 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:9.57,15.2 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:17.25,19.8 2 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:19.8,21.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:23.2,23.26 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:26.38,28.23 2 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:28.23,29.68 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:29.68,31.4 1 0
|
||||
code.squareroundforest.org/arpio/wand/command.go:33.3,33.19 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:36.2,36.12 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:39.46,40.18 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:54.18,55.13 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:57.17,59.30 2 0
|
||||
code.squareroundforest.org/arpio/wand/command.go:60.25,61.24 1 0
|
||||
code.squareroundforest.org/arpio/wand/command.go:61.24,63.4 1 0
|
||||
code.squareroundforest.org/arpio/wand/command.go:65.3,65.13 1 0
|
||||
code.squareroundforest.org/arpio/wand/command.go:66.10,67.57 1 0
|
||||
code.squareroundforest.org/arpio/wand/command.go:71.61,73.23 2 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:73.23,74.47 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:74.47,76.4 1 0
|
||||
code.squareroundforest.org/arpio/wand/command.go:79.2,81.18 3 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:81.18,83.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:85.2,85.38 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:85.38,91.3 1 0
|
||||
code.squareroundforest.org/arpio/wand/command.go:93.2,93.55 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:93.55,99.3 1 0
|
||||
code.squareroundforest.org/arpio/wand/command.go:101.2,101.38 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:101.38,107.3 1 0
|
||||
code.squareroundforest.org/arpio/wand/command.go:109.2,109.37 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:109.37,115.3 1 0
|
||||
code.squareroundforest.org/arpio/wand/command.go:117.2,117.12 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:120.34,124.30 4 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:124.30,126.3 1 0
|
||||
code.squareroundforest.org/arpio/wand/command.go:128.2,130.42 3 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:130.42,132.3 1 0
|
||||
code.squareroundforest.org/arpio/wand/command.go:134.2,134.84 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:134.84,136.3 1 0
|
||||
code.squareroundforest.org/arpio/wand/command.go:138.2,138.12 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:141.40,144.32 3 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:144.32,148.3 1 0
|
||||
code.squareroundforest.org/arpio/wand/command.go:150.2,150.46 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:150.46,153.27 3 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:153.27,155.4 1 0
|
||||
code.squareroundforest.org/arpio/wand/command.go:157.3,157.51 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:157.51,159.4 1 0
|
||||
code.squareroundforest.org/arpio/wand/command.go:161.3,161.26 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:161.26,163.4 1 0
|
||||
code.squareroundforest.org/arpio/wand/command.go:165.3,165.39 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:165.39,167.4 1 0
|
||||
code.squareroundforest.org/arpio/wand/command.go:169.3,169.14 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:172.2,172.12 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:175.37,176.19 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:176.19,178.6 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:180.2,180.21 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:180.21,181.43 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:181.43,183.4 1 0
|
||||
code.squareroundforest.org/arpio/wand/command.go:186.2,186.50 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:186.50,188.3 1 0
|
||||
code.squareroundforest.org/arpio/wand/command.go:190.5,190.24 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:190.24,191.55 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:191.55,193.10 1 0
|
||||
code.squareroundforest.org/arpio/wand/command.go:196.5,198.36 3 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:198.36,199.19 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:199.19,201.4 1 0
|
||||
code.squareroundforest.org/arpio/wand/command.go:203.3,203.20 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:203.20,205.4 1 0
|
||||
code.squareroundforest.org/arpio/wand/command.go:207.3,208.44 2 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:208.44,210.4 1 0
|
||||
code.squareroundforest.org/arpio/wand/command.go:212.9,212.38 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:212.38,214.10 1 0
|
||||
code.squareroundforest.org/arpio/wand/command.go:216.9,216.24 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:216.24,218.10 1 1
|
||||
code.squareroundforest.org/arpio/wand/command.go:221.2,221.12 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:28.30,30.2 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:32.34,34.2 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:36.48,37.29 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:37.29,38.24 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:38.24,40.10 1 0
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:43.5,43.33 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:46.56,47.35 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:47.35,48.22 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:48.22,50.10 1 0
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:53.5,53.35 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:56.36,65.23 8 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:65.23,67.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:69.5,71.46 3 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:71.46,74.3 2 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:76.2,77.23 2 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:77.23,78.28 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:78.28,80.4 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:83.5,84.25 2 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:87.32,89.17 2 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:89.17,91.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:93.2,93.27 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:93.27,95.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:97.2,97.28 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:97.28,99.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:101.2,101.26 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:101.26,102.25 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:102.25,103.12 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:106.3,106.25 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:106.25,107.12 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:110.3,110.15 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:110.15,111.12 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:114.3,114.15 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:114.15,116.4 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:118.3,118.15 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:121.2,121.13 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:124.40,126.16 2 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:126.16,128.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:130.2,130.17 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:130.17,132.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:134.2,134.28 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:134.28,136.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:138.2,138.26 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:138.26,139.15 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:139.15,141.4 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:143.3,143.26 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:143.26,145.4 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:148.2,148.13 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:151.34,152.24 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:152.24,154.6 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:156.5,156.40 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:156.40,157.25 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:157.25,159.10 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:162.5,162.15 1 0
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:165.51,166.40 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:166.40,167.28 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:167.28,169.10 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:172.5,172.24 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:175.70,176.23 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:176.23,178.6 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:180.5,181.12 2 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:181.12,183.6 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:185.5,185.18 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:185.18,188.6 2 0
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:190.5,192.34 3 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:195.49,200.2 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:202.46,207.2 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:209.33,212.2 2 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:214.34,215.17 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:215.17,217.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:219.2,219.19 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:219.19,221.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:223.2,223.27 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:223.27,225.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:227.2,227.13 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:230.38,233.2 2 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:235.85,237.14 2 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:237.14,239.65 2 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:239.65,242.4 2 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:244.3,244.40 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:247.2,252.19 2 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:252.19,256.3 3 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:258.2,258.39 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:258.39,260.25 2 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:260.25,263.4 2 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:265.3,265.38 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:268.2,268.21 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:268.21,270.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:272.2,272.42 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:275.95,278.14 3 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:278.14,280.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:282.2,283.31 2 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:283.31,285.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:287.2,287.94 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:287.94,290.3 2 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:292.2,295.16 4 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:298.55,300.20 2 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:300.20,302.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:304.2,305.9 2 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:306.19,307.20 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:307.20,311.4 3 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:312.21,316.35 4 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:317.29,321.38 4 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:322.10,323.43 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:326.2,329.10 4 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:332.46,334.24 2 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:334.24,336.6 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:338.5,339.49 2 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:339.49,342.6 2 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:344.5,344.26 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:344.26,345.29 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:345.29,346.21 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:349.9,349.30 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:349.30,350.41 1 0
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:350.41,352.14 1 0
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:355.9,355.43 1 1
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:355.43,356.25 1 0
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:356.25,358.14 1 0
|
||||
code.squareroundforest.org/arpio/wand/commandline.go:362.5,362.17 1 1
|
||||
code.squareroundforest.org/arpio/wand/env.go:13.39,20.30 2 1
|
||||
code.squareroundforest.org/arpio/wand/env.go:20.30,21.13 1 1
|
||||
code.squareroundforest.org/arpio/wand/env.go:21.13,24.12 3 1
|
||||
code.squareroundforest.org/arpio/wand/env.go:27.3,27.16 1 1
|
||||
code.squareroundforest.org/arpio/wand/env.go:27.16,29.12 2 1
|
||||
code.squareroundforest.org/arpio/wand/env.go:32.3,32.15 1 1
|
||||
code.squareroundforest.org/arpio/wand/env.go:32.15,35.12 3 1
|
||||
code.squareroundforest.org/arpio/wand/env.go:38.3,38.31 1 1
|
||||
code.squareroundforest.org/arpio/wand/env.go:41.2,42.15 2 1
|
||||
code.squareroundforest.org/arpio/wand/env.go:45.50,52.26 3 1
|
||||
code.squareroundforest.org/arpio/wand/env.go:52.26,54.22 2 1
|
||||
code.squareroundforest.org/arpio/wand/env.go:54.22,55.12 1 1
|
||||
code.squareroundforest.org/arpio/wand/env.go:58.3,60.73 3 1
|
||||
code.squareroundforest.org/arpio/wand/env.go:60.73,61.12 1 1
|
||||
code.squareroundforest.org/arpio/wand/env.go:64.3,66.39 3 1
|
||||
code.squareroundforest.org/arpio/wand/env.go:69.2,69.10 1 1
|
||||
code.squareroundforest.org/arpio/wand/exec.go:11.82,15.45 4 1
|
||||
code.squareroundforest.org/arpio/wand/exec.go:15.45,16.13 1 0
|
||||
code.squareroundforest.org/arpio/wand/exec.go:19.2,22.21 4 1
|
||||
code.squareroundforest.org/arpio/wand/exec.go:22.21,27.3 4 0
|
||||
code.squareroundforest.org/arpio/wand/exec.go:29.5,29.26 1 1
|
||||
code.squareroundforest.org/arpio/wand/exec.go:29.26,32.6 2 0
|
||||
code.squareroundforest.org/arpio/wand/exec.go:34.2,36.39 3 1
|
||||
code.squareroundforest.org/arpio/wand/exec.go:36.39,39.6 2 0
|
||||
code.squareroundforest.org/arpio/wand/exec.go:41.2,41.50 1 1
|
||||
code.squareroundforest.org/arpio/wand/exec.go:41.50,46.3 4 1
|
||||
code.squareroundforest.org/arpio/wand/exec.go:48.2,49.16 2 1
|
||||
code.squareroundforest.org/arpio/wand/exec.go:49.16,53.3 3 0
|
||||
code.squareroundforest.org/arpio/wand/exec.go:55.2,55.52 1 1
|
||||
code.squareroundforest.org/arpio/wand/exec.go:55.52,59.3 3 0
|
||||
code.squareroundforest.org/arpio/wand/help.go:9.17,14.2 1 1
|
||||
code.squareroundforest.org/arpio/wand/help.go:16.30,18.40 2 1
|
||||
code.squareroundforest.org/arpio/wand/help.go:18.40,20.31 2 1
|
||||
code.squareroundforest.org/arpio/wand/help.go:20.31,22.10 1 0
|
||||
code.squareroundforest.org/arpio/wand/help.go:25.5,25.20 1 1
|
||||
code.squareroundforest.org/arpio/wand/help.go:25.20,27.6 1 1
|
||||
code.squareroundforest.org/arpio/wand/help.go:29.5,29.15 1 1
|
||||
code.squareroundforest.org/arpio/wand/help.go:32.38,33.40 1 1
|
||||
code.squareroundforest.org/arpio/wand/help.go:33.40,34.22 1 1
|
||||
code.squareroundforest.org/arpio/wand/help.go:34.22,36.10 1 1
|
||||
code.squareroundforest.org/arpio/wand/help.go:39.5,39.17 1 0
|
||||
code.squareroundforest.org/arpio/wand/help.go:42.40,46.2 3 0
|
||||
code.squareroundforest.org/arpio/wand/help.go:48.64,49.31 1 1
|
||||
code.squareroundforest.org/arpio/wand/help.go:49.31,52.6 2 1
|
||||
code.squareroundforest.org/arpio/wand/help.go:54.5,54.34 1 0
|
||||
code.squareroundforest.org/arpio/wand/help.go:54.34,57.6 2 0
|
||||
code.squareroundforest.org/arpio/wand/help.go:60.62,61.2 0 0
|
||||
code.squareroundforest.org/arpio/wand/input.go:8.40,10.37 2 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:10.37,12.10 2 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:12.10,13.12 1 0
|
||||
code.squareroundforest.org/arpio/wand/input.go:16.3,16.24 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:16.24,17.46 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:17.46,23.5 1 0
|
||||
code.squareroundforest.org/arpio/wand/input.go:25.4,25.29 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:25.29,26.28 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:26.28,31.6 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:36.2,36.12 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:39.49,42.46 3 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:42.46,46.3 3 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:48.2,49.23 2 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:49.23,51.42 2 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:51.42,53.4 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:55.3,55.28 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:58.2,59.24 2 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:59.24,61.24 2 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:61.24,63.27 2 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:63.27,65.5 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:67.4,67.42 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:67.42,73.5 1 0
|
||||
code.squareroundforest.org/arpio/wand/input.go:75.4,75.26 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:75.26,76.57 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:76.57,81.6 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:83.5,83.59 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:83.59,88.6 1 0
|
||||
code.squareroundforest.org/arpio/wand/input.go:93.2,93.12 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:96.56,104.18 8 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:104.18,107.3 2 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:109.2,109.29 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:109.29,111.3 1 0
|
||||
code.squareroundforest.org/arpio/wand/input.go:113.2,113.27 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:113.27,115.3 1 0
|
||||
code.squareroundforest.org/arpio/wand/input.go:117.2,117.18 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:117.18,119.3 1 0
|
||||
code.squareroundforest.org/arpio/wand/input.go:121.2,121.30 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:121.30,123.3 1 0
|
||||
code.squareroundforest.org/arpio/wand/input.go:125.2,125.23 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:125.23,127.18 2 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:127.18,129.4 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:129.9,131.4 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:133.3,133.23 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:133.23,139.4 1 0
|
||||
code.squareroundforest.org/arpio/wand/input.go:142.2,142.12 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:145.58,146.44 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:146.44,148.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:150.2,150.57 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:150.57,152.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:154.2,154.67 1 1
|
||||
code.squareroundforest.org/arpio/wand/input.go:154.67,156.3 1 0
|
||||
code.squareroundforest.org/arpio/wand/input.go:158.2,158.12 1 1
|
||||
code.squareroundforest.org/arpio/wand/output.go:8.46,9.23 1 1
|
||||
code.squareroundforest.org/arpio/wand/output.go:9.23,11.9 2 1
|
||||
code.squareroundforest.org/arpio/wand/output.go:11.9,12.43 1 0
|
||||
code.squareroundforest.org/arpio/wand/output.go:12.43,14.5 1 0
|
||||
code.squareroundforest.org/arpio/wand/output.go:16.4,16.12 1 0
|
||||
code.squareroundforest.org/arpio/wand/output.go:19.3,19.55 1 1
|
||||
code.squareroundforest.org/arpio/wand/output.go:19.55,21.4 1 0
|
||||
code.squareroundforest.org/arpio/wand/output.go:24.2,24.12 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:20.58,21.19 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:21.19,23.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:25.2,25.33 1 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:25.33,30.3 4 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:32.2,35.10 4 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:38.37,39.18 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:41.17,42.26 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:43.10,44.11 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:48.36,51.2 2 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:53.45,54.18 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:55.20,57.20 2 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:58.78,60.20 2 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:61.83,63.20 2 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:64.40,66.20 2 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:67.22,68.14 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:69.10,70.15 1 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:74.41,76.18 2 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:77.20,79.46 2 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:80.78,82.46 2 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:83.83,85.46 2 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:86.40,88.46 2 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:89.10,90.46 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:93.2,93.29 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:96.40,97.17 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:97.17,99.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:101.2,106.39 2 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:106.39,113.21 7 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:127.19,128.86 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:129.26,130.28 1 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:130.28,132.5 1 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:133.23,135.20 2 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:135.20,137.5 1 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:137.10,138.24 1 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:138.24,141.6 2 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:143.5,143.46 1 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:148.2,149.32 2 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:149.32,151.3 1 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:153.2,153.33 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:153.33,155.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:157.2,158.24 2 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:158.24,160.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:162.2,162.39 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:165.36,167.23 2 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:167.23,168.36 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:168.36,170.4 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:173.2,173.10 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:176.45,183.23 7 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:183.23,185.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:187.2,187.11 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:190.81,192.33 2 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:192.33,195.11 3 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:195.11,197.4 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:200.2,200.10 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:203.58,204.55 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:204.55,206.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:209.54,210.55 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:210.55,212.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:215.46,216.16 1 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:216.16,218.3 1 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:220.2,222.19 3 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:223.20,224.35 1 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:225.78,226.20 1 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:227.79,228.15 1 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:229.11,230.16 1 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:232.83,233.20 1 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:234.84,235.15 1 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:236.11,237.16 1 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:239.40,240.20 1 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:241.41,242.15 1 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:243.11,244.16 1 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:246.22,247.37 1 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:248.25,249.86 1 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:250.10,251.15 1 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:255.43,256.31 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:256.31,258.3 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:260.2,260.18 1 1
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:261.38,262.35 1 0
|
||||
code.squareroundforest.org/arpio/wand/reflect.go:263.10,264.15 1 1
|
||||
code.squareroundforest.org/arpio/wand/wand.go:17.57,19.2 1 1
|
||||
code.squareroundforest.org/arpio/wand/wand.go:21.27,24.2 2 1
|
||||
code.squareroundforest.org/arpio/wand/wand.go:26.39,31.2 4 0
|
||||
code.squareroundforest.org/arpio/wand/wand.go:33.43,37.2 3 1
|
||||
code.squareroundforest.org/arpio/wand/wand.go:39.21,42.2 2 0
|
||||
21
Makefile
Normal file
21
Makefile
Normal file
@ -0,0 +1,21 @@
|
||||
SOURCES = $(shell find . -name "*.go")
|
||||
|
||||
default: build
|
||||
|
||||
build: $(SOURCES)
|
||||
go build ./...
|
||||
|
||||
check: $(SOURCES)
|
||||
go test -count 1 ./...
|
||||
|
||||
.cover: $(SOURCES)
|
||||
go test -count 1 -coverprofile .cover ./...
|
||||
|
||||
cover: .cover
|
||||
go tool cover -func .cover
|
||||
|
||||
showcover: .cover
|
||||
go tool cover -html .cover
|
||||
|
||||
fmt: $(SOURCES)
|
||||
go fmt ./...
|
||||
234
apply.go
Normal file
234
apply.go
Normal file
@ -0,0 +1,234 @@
|
||||
package wand
|
||||
|
||||
import (
|
||||
"github.com/iancoleman/strcase"
|
||||
"reflect"
|
||||
"strings"
|
||||
"os"
|
||||
)
|
||||
|
||||
func ensurePointerAllocation(p reflect.Value, n int) {
|
||||
if p.IsNil() {
|
||||
p.Set(reflect.New(p.Type().Elem()))
|
||||
}
|
||||
|
||||
ensureAllocation(p.Elem(), n)
|
||||
}
|
||||
|
||||
func ensureSliceAllocation(s reflect.Value, n int) {
|
||||
if s.Len() < n {
|
||||
a := reflect.MakeSlice(s.Type(), n-s.Len(), n-s.Len())
|
||||
a = reflect.AppendSlice(s, a)
|
||||
s.Set(a)
|
||||
}
|
||||
|
||||
if s.Len() > n {
|
||||
a := s.Slice(0, n)
|
||||
s.Set(a)
|
||||
}
|
||||
|
||||
for i := 0; i < s.Len(); i++ {
|
||||
ensureAllocation(s.Index(i), 1)
|
||||
}
|
||||
}
|
||||
|
||||
func ensureAllocation(v reflect.Value, n int) {
|
||||
switch v.Type().Kind() {
|
||||
case reflect.Pointer:
|
||||
ensurePointerAllocation(v, n)
|
||||
case reflect.Slice:
|
||||
ensureSliceAllocation(v, n)
|
||||
}
|
||||
}
|
||||
|
||||
func setPointerValue(p reflect.Value, v []value) {
|
||||
setFieldValue(p.Elem(), v)
|
||||
}
|
||||
|
||||
func setSliceValue(s reflect.Value, v []value) {
|
||||
for i := 0; i < s.Len(); i++ {
|
||||
setFieldValue(s.Index(i), v[i:i+1])
|
||||
}
|
||||
}
|
||||
|
||||
func setValue(f reflect.Value, v value) {
|
||||
if v.isBool {
|
||||
f.Set(reflect.ValueOf(v.boolean))
|
||||
return
|
||||
}
|
||||
|
||||
f.Set(reflect.ValueOf(scan(f.Type(), v.str)))
|
||||
}
|
||||
|
||||
func setFieldValue(field reflect.Value, v []value) {
|
||||
switch field.Kind() {
|
||||
case reflect.Pointer:
|
||||
setPointerValue(field, v)
|
||||
case reflect.Slice:
|
||||
setSliceValue(field, v)
|
||||
default:
|
||||
setValue(field, v[0])
|
||||
}
|
||||
}
|
||||
|
||||
func setField(s reflect.Value, name string, v []value) {
|
||||
for i := 0; i < s.Type().NumField(); i++ {
|
||||
fs := s.Type().Field(i)
|
||||
fname := strcase.ToKebab(fs.Name)
|
||||
ft := fs.Type
|
||||
ftup := unpack(ft)
|
||||
fv := s.Field(i)
|
||||
switch {
|
||||
case !fs.Anonymous && fname == name:
|
||||
ensureAllocation(fv, len(v))
|
||||
setFieldValue(fv, v)
|
||||
case !fs.Anonymous && ftup.Kind() == reflect.Struct:
|
||||
prefix := fname + "-"
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
ensureAllocation(fv, len(v))
|
||||
setField(unpack(fv), name[len(prefix):], v)
|
||||
}
|
||||
case fs.Anonymous:
|
||||
ensureAllocation(fv, 1)
|
||||
setField(unpack(fv), name, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createStructArg(t reflect.Type, shortForms []string, e env, o []option) (reflect.Value, bool) {
|
||||
tup := unpack(t)
|
||||
f := fields(tup)
|
||||
fn := make(map[string]bool)
|
||||
for _, fi := range f {
|
||||
fn[fi.name] = true
|
||||
}
|
||||
|
||||
ms := make(map[string]string)
|
||||
for i := 0; i < len(shortForms); i += 2 {
|
||||
l, s := shortForms[i], shortForms[i+1]
|
||||
ms[s] = l
|
||||
}
|
||||
|
||||
om := make(map[string][]option)
|
||||
for _, oi := range o {
|
||||
n := oi.name
|
||||
if l, ok := ms[n]; ok && oi.shortForm {
|
||||
n = l
|
||||
}
|
||||
|
||||
om[n] = append(om[n], oi)
|
||||
}
|
||||
|
||||
var foundEnv []string
|
||||
for n := range e.values {
|
||||
if fn[n] {
|
||||
foundEnv = append(foundEnv, n)
|
||||
}
|
||||
}
|
||||
|
||||
var foundOptions []string
|
||||
for n := range om {
|
||||
if fn[n] {
|
||||
foundOptions = append(foundOptions, n)
|
||||
}
|
||||
}
|
||||
|
||||
if len(foundEnv) == 0 && len(foundOptions) == 0 {
|
||||
return reflect.Zero(t), false
|
||||
}
|
||||
|
||||
p := reflect.New(tup)
|
||||
for _, n := range foundEnv {
|
||||
var v []value
|
||||
for _, vi := range e.values[n] {
|
||||
v = append(v, stringValue(vi))
|
||||
}
|
||||
|
||||
setField(p.Elem(), n, v)
|
||||
}
|
||||
|
||||
for _, n := range foundOptions {
|
||||
var v []value
|
||||
for _, oi := range om[n] {
|
||||
v = append(v, oi.value)
|
||||
}
|
||||
|
||||
setField(p.Elem(), n, v)
|
||||
}
|
||||
|
||||
return pack(p.Elem(), t), true
|
||||
}
|
||||
|
||||
func createPositional(t reflect.Type, v string) reflect.Value {
|
||||
tup := unpack(t)
|
||||
sv := reflect.ValueOf(scan(tup, v))
|
||||
return pack(sv, t)
|
||||
}
|
||||
|
||||
func createArgs(t reflect.Type, shortForms []string, e env, cl commandLine) []reflect.Value {
|
||||
var args []reflect.Value
|
||||
positional := cl.positional
|
||||
for i := 0; i < t.NumIn(); i++ {
|
||||
ti := t.In(i)
|
||||
structure := isStruct(ti)
|
||||
variadic := t.IsVariadic() && i == t.NumIn()-1
|
||||
ior := isReader(ti)
|
||||
iow := isWriter(ti)
|
||||
switch {
|
||||
case ior:
|
||||
args = append(args, reflect.ValueOf(os.Stdin))
|
||||
case iow:
|
||||
args = append(args, reflect.ValueOf(os.Stdout))
|
||||
case structure && variadic:
|
||||
if arg, ok := createStructArg(ti, shortForms, e, cl.options); ok {
|
||||
args = append(args, arg)
|
||||
}
|
||||
case structure:
|
||||
arg, _ := createStructArg(ti, shortForms, e, cl.options)
|
||||
args = append(args, arg)
|
||||
case variadic:
|
||||
for _, p := range positional {
|
||||
args = append(args, createPositional(ti.Elem(), p))
|
||||
}
|
||||
default:
|
||||
var p string
|
||||
p, positional = positional[0], positional[1:]
|
||||
args = append(args, createPositional(ti, p))
|
||||
}
|
||||
}
|
||||
|
||||
return args
|
||||
}
|
||||
|
||||
func processResults(t reflect.Type, out []reflect.Value) ([]any, error) {
|
||||
if len(out) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var err error
|
||||
last := len(out) - 1
|
||||
isErrorType := t.Out(last) == reflect.TypeOf(err)
|
||||
if isErrorType && !out[last].IsZero() {
|
||||
err = out[last].Interface().(error)
|
||||
}
|
||||
|
||||
if isErrorType {
|
||||
out = out[:last]
|
||||
}
|
||||
|
||||
var values []any
|
||||
for _, o := range out {
|
||||
values = append(values, o.Interface())
|
||||
}
|
||||
|
||||
return values, err
|
||||
}
|
||||
|
||||
func apply(cmd Cmd, e env, cl commandLine) ([]any, error) {
|
||||
v := reflect.ValueOf(cmd.impl)
|
||||
v = unpack(v)
|
||||
t := v.Type()
|
||||
args := createArgs(t, cmd.shortForms, e, cl)
|
||||
out := v.Call(args)
|
||||
return processResults(t, out)
|
||||
}
|
||||
12
cmd/wand-docs/main.go
Normal file
12
cmd/wand-docs/main.go
Normal file
@ -0,0 +1,12 @@
|
||||
package main
|
||||
|
||||
// myFunc is.
|
||||
func myFunc() {
|
||||
}
|
||||
|
||||
// MyFunc is.
|
||||
func MyFunc() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
}
|
||||
236
command.go
Normal file
236
command.go
Normal file
@ -0,0 +1,236 @@
|
||||
package wand
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
)
|
||||
|
||||
func command(name string, impl any, subcmds ...Cmd) Cmd {
|
||||
return Cmd{
|
||||
name: name,
|
||||
impl: impl,
|
||||
subcommands: subcmds,
|
||||
}
|
||||
}
|
||||
|
||||
func wrap(impl any) Cmd {
|
||||
cmd, ok := impl.(Cmd)
|
||||
if ok {
|
||||
return cmd
|
||||
}
|
||||
|
||||
return Command("", impl)
|
||||
}
|
||||
|
||||
func validateFields(f []field) error {
|
||||
mf := make(map[string]field)
|
||||
for _, fi := range f {
|
||||
if ef, ok := mf[fi.name]; ok && !compatibleTypes(fi.typ, ef.typ) {
|
||||
return fmt.Errorf("duplicate fields with different types: %s", fi.name)
|
||||
}
|
||||
|
||||
mf[fi.name] = fi
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateParameter(t reflect.Type) error {
|
||||
switch t.Kind() {
|
||||
case reflect.Bool,
|
||||
reflect.Int,
|
||||
reflect.Int8,
|
||||
reflect.Int16,
|
||||
reflect.Int32,
|
||||
reflect.Int64,
|
||||
reflect.Uint,
|
||||
reflect.Uint8,
|
||||
reflect.Uint16,
|
||||
reflect.Uint32,
|
||||
reflect.Uint64,
|
||||
reflect.Float32,
|
||||
reflect.Float64,
|
||||
reflect.String:
|
||||
return nil
|
||||
case reflect.Pointer,
|
||||
reflect.Slice:
|
||||
t = unpack(t)
|
||||
return validateParameter(t)
|
||||
case reflect.Interface:
|
||||
if t.NumMethod() > 0 {
|
||||
return errors.New("'non-empty' interface parameter")
|
||||
}
|
||||
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("unsupported parameter type: %v", t)
|
||||
}
|
||||
}
|
||||
|
||||
func validatePositional(t reflect.Type, min, max int) error {
|
||||
p := positionalParameters(t)
|
||||
ior, iow := ioParameters(p)
|
||||
if len(ior) > 1 || len(iow) > 1 {
|
||||
return errors.New("only zero or one reader and zero or one writer parameters is supported")
|
||||
}
|
||||
|
||||
for i, pi := range p {
|
||||
if slices.Contains(ior, i) || slices.Contains(iow, i) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := validateParameter(pi); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
last := t.NumIn()-1
|
||||
lastVariadic := t.IsVariadic() &&
|
||||
!isStruct(t.In(last)) &&
|
||||
!slices.Contains(ior, last) &&
|
||||
!slices.Contains(iow, last)
|
||||
fixedPositional := len(p) - len(ior) - len(iow)
|
||||
if lastVariadic {
|
||||
fixedPositional--
|
||||
}
|
||||
|
||||
if min > 0 && min < fixedPositional {
|
||||
return fmt.Errorf(
|
||||
"minimum positional defined as %d but the implementation expects minimum %d fixed parameters",
|
||||
min,
|
||||
fixedPositional,
|
||||
)
|
||||
}
|
||||
|
||||
if min > 0 && min > fixedPositional && !lastVariadic {
|
||||
return fmt.Errorf(
|
||||
"minimum positional defined as %d but the implementation has only %d fixed parameters and no variadic parameter",
|
||||
min,
|
||||
fixedPositional,
|
||||
)
|
||||
}
|
||||
|
||||
if max > 0 && max < fixedPositional {
|
||||
return fmt.Errorf(
|
||||
"maximum positional defined as %d but the implementation expects minimum %d fixed parameters",
|
||||
max,
|
||||
fixedPositional,
|
||||
)
|
||||
}
|
||||
|
||||
if min > 0 && max > 0 && min > max {
|
||||
return fmt.Errorf(
|
||||
"minimum positional defined as larger then the maxmimum positional: %d > %d",
|
||||
min,
|
||||
max,
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateImpl(cmd Cmd) error {
|
||||
v := reflect.ValueOf(cmd.impl)
|
||||
v = unpack(v)
|
||||
t := v.Type()
|
||||
if t.Kind() != reflect.Func {
|
||||
return errors.New("command implementation not a function")
|
||||
}
|
||||
|
||||
s := structParameters(t)
|
||||
f := fields(s...)
|
||||
if err := validateFields(f); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validatePositional(t, cmd.minPositional, cmd.maxPositional); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateShortForms(cmd Cmd) error {
|
||||
mf := mapFields(cmd.impl)
|
||||
ms := make(map[string]string)
|
||||
if len(cmd.shortForms)%2 != 0 {
|
||||
return fmt.Errorf(
|
||||
"undefined option short form: %s", cmd.shortForms[len(cmd.shortForms)-1],
|
||||
)
|
||||
}
|
||||
|
||||
for i := 0; i < len(cmd.shortForms); i += 2 {
|
||||
fn := cmd.shortForms[i]
|
||||
sf := cmd.shortForms[i+1]
|
||||
if _, ok := mf[fn]; !ok {
|
||||
return fmt.Errorf("undefined field: %s", fn)
|
||||
}
|
||||
|
||||
if len(sf) != 1 && (sf[0] < 'a' || sf[0] > 'z') {
|
||||
return fmt.Errorf("invalid short form: %s", sf)
|
||||
}
|
||||
|
||||
if _, ok := mf[sf]; ok {
|
||||
return fmt.Errorf("short form shadowing field name: %s", sf)
|
||||
}
|
||||
|
||||
if lf, ok := ms[sf]; ok && lf != fn {
|
||||
return fmt.Errorf("ambigous short form: %s", sf)
|
||||
}
|
||||
|
||||
ms[sf] = fn
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateCommand(cmd Cmd) error {
|
||||
if cmd.isHelp {
|
||||
return nil
|
||||
}
|
||||
|
||||
if cmd.impl != nil {
|
||||
if err := validateImpl(cmd); err != nil {
|
||||
return fmt.Errorf("%s: %w", cmd.name, err)
|
||||
}
|
||||
}
|
||||
|
||||
if cmd.impl == nil && len(cmd.subcommands) == 0 {
|
||||
return fmt.Errorf("empty command category: %s", cmd.name)
|
||||
}
|
||||
|
||||
if cmd.impl != nil {
|
||||
if err := validateShortForms(cmd); err != nil {
|
||||
return fmt.Errorf("%s: %w", cmd.name, err)
|
||||
}
|
||||
}
|
||||
|
||||
var hasDefault bool
|
||||
names := make(map[string]bool)
|
||||
for _, s := range cmd.subcommands {
|
||||
if s.name == "" {
|
||||
return fmt.Errorf("unnamed subcommand of: %s", cmd.name)
|
||||
}
|
||||
|
||||
if names[s.name] {
|
||||
return fmt.Errorf("subcommand name conflict: %s", s.name)
|
||||
}
|
||||
|
||||
names[s.name] = true
|
||||
if err := validateCommand(s); err != nil {
|
||||
return fmt.Errorf("%s: %w", s.name, err)
|
||||
}
|
||||
|
||||
if s.isDefault && hasDefault {
|
||||
return fmt.Errorf("multiple default subcommands for: %s", cmd.name)
|
||||
}
|
||||
|
||||
if s.isDefault {
|
||||
hasDefault = true
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
363
commandline.go
Normal file
363
commandline.go
Normal file
@ -0,0 +1,363 @@
|
||||
package wand
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
type value struct {
|
||||
isBool bool
|
||||
boolean bool
|
||||
str string
|
||||
}
|
||||
|
||||
type option struct {
|
||||
name string
|
||||
value value
|
||||
shortForm bool
|
||||
}
|
||||
|
||||
type commandLine struct {
|
||||
options []option
|
||||
positional []string
|
||||
}
|
||||
|
||||
func boolValue(b bool) value {
|
||||
return value{isBool: true, boolean: b}
|
||||
}
|
||||
|
||||
func stringValue(s string) value {
|
||||
return value{str: s}
|
||||
}
|
||||
|
||||
func insertHelpOption(names []string) []string {
|
||||
for _, n := range names {
|
||||
if n == "help" {
|
||||
return names
|
||||
}
|
||||
}
|
||||
|
||||
return append(names, "help")
|
||||
}
|
||||
|
||||
func insertHelpShortForm(shortForms []string) []string {
|
||||
for _, sf := range shortForms {
|
||||
if sf == "h" {
|
||||
return shortForms
|
||||
}
|
||||
}
|
||||
|
||||
return append(shortForms, "h")
|
||||
}
|
||||
|
||||
func boolOptions(cmd Cmd) []string {
|
||||
v := reflect.ValueOf(cmd.impl)
|
||||
v = unpack(v)
|
||||
t := v.Type()
|
||||
s := structParameters(t)
|
||||
f := fields(s...)
|
||||
b := boolFields(f)
|
||||
|
||||
var n []string
|
||||
for _, fi := range b {
|
||||
n = append(n, fi.name)
|
||||
}
|
||||
|
||||
n = insertHelpOption(n)
|
||||
sfm := make(map[string][]string)
|
||||
for i := 0; i < len(cmd.shortForms); i += 2 {
|
||||
l, s := cmd.shortForms[i], cmd.shortForms[i+1]
|
||||
sfm[l] = append(sfm[l], s)
|
||||
}
|
||||
|
||||
var sf []string
|
||||
for _, ni := range n {
|
||||
if sn, ok := sfm[ni]; ok {
|
||||
sf = append(sf, sn...)
|
||||
}
|
||||
}
|
||||
|
||||
sf = insertHelpShortForm(sf)
|
||||
return append(n, sf...)
|
||||
}
|
||||
|
||||
func isOption(arg string) bool {
|
||||
a := []rune(arg)
|
||||
if len(a) <= 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
if string(a[:2]) != "--" {
|
||||
return false
|
||||
}
|
||||
|
||||
if !unicode.IsLower(a[2]) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, r := range a[3:] {
|
||||
if unicode.IsLower(r) {
|
||||
continue
|
||||
}
|
||||
|
||||
if unicode.IsDigit(r) {
|
||||
continue
|
||||
}
|
||||
|
||||
if r == '-' {
|
||||
continue
|
||||
}
|
||||
|
||||
if r == '=' {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func isShortOptionSet(arg string) bool {
|
||||
a := []rune(arg)
|
||||
if len(a) < 2 {
|
||||
return false
|
||||
}
|
||||
|
||||
if a[0] != '-' {
|
||||
return false
|
||||
}
|
||||
|
||||
if !unicode.IsLower(a[1]) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, r := range a[2:] {
|
||||
if r == '=' {
|
||||
return true
|
||||
}
|
||||
|
||||
if !unicode.IsLower(r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func defaultCommand(cmd Cmd) Cmd {
|
||||
if cmd.impl != nil {
|
||||
return cmd
|
||||
}
|
||||
|
||||
for _, sc := range cmd.subcommands {
|
||||
if sc.isDefault {
|
||||
return sc
|
||||
}
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func subcommand(cmd Cmd, name string) (Cmd, bool) {
|
||||
for _, sc := range cmd.subcommands {
|
||||
if sc.name == name {
|
||||
return sc, true
|
||||
}
|
||||
}
|
||||
|
||||
return Cmd{}, false
|
||||
}
|
||||
|
||||
func selectCommand(cmd Cmd, args []string) (Cmd, []string, []string) {
|
||||
if len(args) == 0 {
|
||||
return defaultCommand(cmd), []string{cmd.name}, nil
|
||||
}
|
||||
|
||||
sc, ok := subcommand(cmd, args[0])
|
||||
if !ok {
|
||||
return defaultCommand(cmd), []string{cmd.name}, args
|
||||
}
|
||||
|
||||
if sc.isHelp {
|
||||
cmd.helpRequested = true
|
||||
return cmd, []string{cmd.name}, args[1:]
|
||||
}
|
||||
|
||||
cmd, fullCommand, args := selectCommand(sc, args[1:])
|
||||
fullCommand = append([]string{cmd.name}, fullCommand...)
|
||||
return cmd, fullCommand, args
|
||||
}
|
||||
|
||||
func boolOption(name string, value bool) option {
|
||||
return option{
|
||||
name: name,
|
||||
value: boolValue(value),
|
||||
}
|
||||
}
|
||||
|
||||
func stringOption(name, value string) option {
|
||||
return option{
|
||||
name: name,
|
||||
value: stringValue(value),
|
||||
}
|
||||
}
|
||||
|
||||
func shortForm(o option) option {
|
||||
o.shortForm = true
|
||||
return o
|
||||
}
|
||||
|
||||
func canBeValue(arg string) bool {
|
||||
if arg == "--" {
|
||||
return false
|
||||
}
|
||||
|
||||
if isOption(arg) {
|
||||
return false
|
||||
}
|
||||
|
||||
if isShortOptionSet(arg) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func canBeBoolValue(arg string) bool {
|
||||
_, err := strconv.ParseBool(arg)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func readOption(boolOptions []string, arg string, args []string) (option, []string) {
|
||||
eqi := strings.Index(arg, "=")
|
||||
if eqi >= 0 {
|
||||
arg, value := arg[:eqi], arg[eqi+1:]
|
||||
if slices.Contains(boolOptions, arg) && canBeBoolValue(value) {
|
||||
v, _ := strconv.ParseBool(value)
|
||||
return boolOption(arg, v), args
|
||||
}
|
||||
|
||||
return stringOption(arg, value), args
|
||||
}
|
||||
|
||||
var (
|
||||
next string
|
||||
nextCanBeValue, nextCanBeBoolValue bool
|
||||
)
|
||||
|
||||
if len(args) > 0 {
|
||||
next = args[0]
|
||||
nextCanBeValue = canBeValue(next)
|
||||
nextCanBeBoolValue = canBeBoolValue(next)
|
||||
}
|
||||
|
||||
if slices.Contains(boolOptions, arg) {
|
||||
value := true
|
||||
if nextCanBeBoolValue {
|
||||
value, _ = strconv.ParseBool(next)
|
||||
args = args[1:]
|
||||
}
|
||||
|
||||
return boolOption(arg, value), args
|
||||
}
|
||||
|
||||
if !nextCanBeValue {
|
||||
return boolOption(arg, true), args
|
||||
}
|
||||
|
||||
return stringOption(arg, next), args[1:]
|
||||
}
|
||||
|
||||
func readShortOptionSet(boolOptions []string, arg string, args []string) ([]option, []string) {
|
||||
last := len(arg) - 1
|
||||
eqi := strings.Index(arg, "=")
|
||||
if eqi >= 0 {
|
||||
last = eqi - 1
|
||||
}
|
||||
|
||||
var o []option
|
||||
for _, a := range arg[:last] {
|
||||
o = append(o, shortForm(boolOption(string(a), true)))
|
||||
}
|
||||
|
||||
if slices.Contains(boolOptions, arg[last:]) && (len(args) == 0 || !canBeBoolValue(args[0])) {
|
||||
o = append(o, shortForm(boolOption(arg[last:], true)))
|
||||
return o, args
|
||||
}
|
||||
|
||||
var lastOption option
|
||||
lastOption, args = readOption(boolOptions, arg[last:], args)
|
||||
o = append(o, shortForm(lastOption))
|
||||
return o, args
|
||||
}
|
||||
|
||||
func readArgs(boolOptions, args []string) commandLine {
|
||||
var c commandLine
|
||||
if len(args) == 0 {
|
||||
return c
|
||||
}
|
||||
|
||||
arg, args := args[0], args[1:]
|
||||
switch {
|
||||
case arg == "--":
|
||||
if len(args) > 0 {
|
||||
arg, args = args[0], args[1:]
|
||||
c.positional = append(c.positional, arg)
|
||||
args = append([]string{"--"}, args...)
|
||||
}
|
||||
case isOption(arg):
|
||||
var f option
|
||||
arg = arg[2:]
|
||||
f, args = readOption(boolOptions, arg, args)
|
||||
c.options = append(c.options, f)
|
||||
case isShortOptionSet(arg):
|
||||
var f []option
|
||||
arg = arg[1:]
|
||||
f, args = readShortOptionSet(boolOptions, arg, args)
|
||||
c.options = append(c.options, f...)
|
||||
default:
|
||||
c.positional = append(c.positional, arg)
|
||||
}
|
||||
|
||||
cc := readArgs(boolOptions, args)
|
||||
c.options = append(c.options, cc.options...)
|
||||
c.positional = append(c.positional, cc.positional...)
|
||||
return c
|
||||
}
|
||||
|
||||
func hasHelpOption(cmd Cmd, o []option) bool {
|
||||
var mf map[string][]field
|
||||
if cmd.impl != nil {
|
||||
mf = mapFields(cmd.impl)
|
||||
}
|
||||
|
||||
sf := make(map[string]bool)
|
||||
for i := 0; i < len(cmd.shortForms); i += 2 {
|
||||
s := cmd.shortForms[i+1]
|
||||
sf[s] = true
|
||||
}
|
||||
|
||||
for _, oi := range o {
|
||||
if !oi.value.isBool {
|
||||
continue
|
||||
}
|
||||
|
||||
if oi.name == "help" {
|
||||
if _, ok := mf["help"]; !ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
if oi.shortForm && oi.name == "h" {
|
||||
if !sf["h"] {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
239
commandline_test.go
Normal file
239
commandline_test.go
Normal file
@ -0,0 +1,239 @@
|
||||
package wand
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCommand(t *testing.T) {
|
||||
type f struct {
|
||||
One string
|
||||
SecondField int
|
||||
}
|
||||
ff := func(f f) string {
|
||||
return f.One + fmt.Sprint(f.SecondField)
|
||||
}
|
||||
|
||||
type b struct{ One, Two, Three, Four bool }
|
||||
fb := func(b b) string {
|
||||
return fmt.Sprintf("%t;%t;%t;%t", b.One, b.Two, b.Three, b.Four)
|
||||
}
|
||||
|
||||
fbp := func(b b, p ...string) string {
|
||||
o := fb(b)
|
||||
if len(p) == 0 {
|
||||
return o
|
||||
}
|
||||
|
||||
s := []string{o}
|
||||
for _, pi := range p {
|
||||
s = append(s, pi)
|
||||
}
|
||||
|
||||
return strings.Join(s, ";")
|
||||
}
|
||||
|
||||
type m struct {
|
||||
One, Two bool
|
||||
Three string
|
||||
}
|
||||
fm := func(m m) string {
|
||||
return fmt.Sprintf("%t;%t;%s", m.One, m.Two, m.Three)
|
||||
}
|
||||
|
||||
type l struct {
|
||||
One []bool
|
||||
Two []string
|
||||
}
|
||||
fl := func(l l) string {
|
||||
var sb []string
|
||||
for _, b := range l.One {
|
||||
sb = append(sb, fmt.Sprint(b))
|
||||
}
|
||||
|
||||
return strings.Join([]string{strings.Join(sb, ","), strings.Join(l.Two, ",")}, ";")
|
||||
}
|
||||
|
||||
type lb struct{ One, Two, Three []bool }
|
||||
flb := func(lb lb) string {
|
||||
var s []string
|
||||
for _, b := range [][]bool{lb.One, lb.Two, lb.Three} {
|
||||
var sb []string
|
||||
for _, bi := range b {
|
||||
sb = append(sb, fmt.Sprint(bi))
|
||||
}
|
||||
|
||||
s = append(s, strings.Join(sb, ","))
|
||||
}
|
||||
|
||||
return strings.Join(s, ";")
|
||||
}
|
||||
|
||||
fp := func(f f, a ...string) string {
|
||||
o := ff(f)
|
||||
return fmt.Sprintf("%s;%s", o, strings.Join(a, ","))
|
||||
}
|
||||
|
||||
type d struct{ One2 bool }
|
||||
fd := func(d d) string {
|
||||
return fmt.Sprint(d.One2)
|
||||
}
|
||||
|
||||
t.Run("no args", testExec(t, ff, "", "foo", "", "0"))
|
||||
t.Run("basic options", func(t *testing.T) {
|
||||
t.Run("space", testExec(t, ff, "", "foo --one baz --second-field 42", "", "baz42"))
|
||||
t.Run("eq", testExec(t, ff, "", "foo --one=baz --second-field=42", "", "baz42"))
|
||||
})
|
||||
|
||||
t.Run("short options combined, explicit last", func(t *testing.T) {
|
||||
t.Run("bool last", testExec(t, ShortForm(fb, "one", "a", "two", "b", "three", "c"), "", "foo -abc true", "", "true;true;true;false"))
|
||||
t.Run("string last", testExec(t, ShortForm(fm, "one", "a", "two", "b", "three", "c"), "", "foo -abc bar", "", "true;true;bar"))
|
||||
})
|
||||
|
||||
t.Run("multiple values", func(t *testing.T) {
|
||||
t.Run("bools, short", testExec(t, ShortForm(fl, "one", "a"), "", "foo -a -a -a", "", "true,true,true;"))
|
||||
t.Run("bools, short, combined", testExec(t, ShortForm(fl, "one", "a"), "", "foo -aaa", "", "true,true,true;"))
|
||||
t.Run("bools, short, explicit", testExec(t, ShortForm(fl, "one", "a"), "", "foo -a true -a true -a true", "", "true,true,true;"))
|
||||
t.Run("bools, short, combined, last explicit", testExec(t, ShortForm(fl, "one", "a"), "", "foo -aaa true", "", "true,true,true;"))
|
||||
t.Run("bools, long", testExec(t, fl, "", "foo --one --one --one", "", "true,true,true;"))
|
||||
t.Run("bools, long, explicit", testExec(t, fl, "", "foo --one true --one true --one true", "", "true,true,true;"))
|
||||
t.Run("mixd, short", testExec(t, ShortForm(fl, "one", "a", "two", "b"), "", "foo -a -b bar", "", "true;bar"))
|
||||
t.Run("mixed, short, combined", testExec(t, ShortForm(fl, "one", "a", "two", "b"), "", "foo -ab bar", "", "true;bar"))
|
||||
t.Run("mixed, long", testExec(t, fl, "", "foo --one --two bar", "", "true;bar"))
|
||||
t.Run("mixed, long, explicit", testExec(t, fl, "", "foo --one true --two bar", "", "true;bar"))
|
||||
})
|
||||
|
||||
t.Run("implicit bool option", func(t *testing.T) {
|
||||
t.Run("short", testExec(t, ShortForm(fb, "one", "a"), "", "foo -a", "", "true;false;false;false"))
|
||||
t.Run(
|
||||
"short, multiple",
|
||||
testExec(t, ShortForm(fb, "one", "a", "two", "b", "three", "c"), "", "foo -a -b -c", "", "true;true;true;false"),
|
||||
)
|
||||
|
||||
t.Run(
|
||||
"short, combined",
|
||||
testExec(t, ShortForm(fb, "one", "a", "two", "b", "three", "c"), "", "foo -abc", "", "true;true;true;false"),
|
||||
)
|
||||
|
||||
t.Run(
|
||||
"short, combined, multiple",
|
||||
testExec(t, ShortForm(fb, "one", "a", "two", "b", "three", "c", "four", "d"), "", "foo -ab -cd", "", "true;true;true;true"),
|
||||
)
|
||||
|
||||
t.Run(
|
||||
"short, multiple values",
|
||||
testExec(t, ShortForm(flb, "one", "a", "two", "b", "three", "c"), "", "foo -aba -cab", "", "true,true,true;true,true;true"),
|
||||
)
|
||||
|
||||
t.Run("long", testExec(t, fb, "", "foo --one", "", "true;false;false;false"))
|
||||
t.Run("long, multiple", testExec(t, fb, "", "foo --one --two --three", "", "true;true;true;false"))
|
||||
t.Run("long, multiple values", testExec(t, flb, "", "foo --one --two --one", "", "true,true;true;"))
|
||||
})
|
||||
|
||||
t.Run("explicit bool option", func(t *testing.T) {
|
||||
t.Run("short, true", testExec(t, ShortForm(fb, "one", "a"), "", "foo -a true", "", "true;false;false;false"))
|
||||
t.Run("short, false", testExec(t, ShortForm(fb, "one", "a"), "", "foo -a false", "", "false;false;false;false"))
|
||||
t.Run("short, with eq", testExec(t, ShortForm(fb, "one", "a"), "", "foo -a=true", "", "true;false;false;false"))
|
||||
t.Run("short, true variant, capital", testExec(t, ShortForm(fb, "one", "a"), "", "foo -a True", "", "true;false;false;false"))
|
||||
t.Run("short, true variant, 1", testExec(t, ShortForm(fb, "one", "a"), "", "foo -a 1", "", "true;false;false;false"))
|
||||
t.Run("short, false variant, 0", testExec(t, ShortForm(fb, "one", "a"), "", "foo -a 0", "", "false;false;false;false"))
|
||||
t.Run("short, combined", testExec(t, ShortForm(fb, "one", "a", "two", "b"), "", "foo -ab true", "", "true;true;false;false"))
|
||||
t.Run(
|
||||
"short, combined, multiple",
|
||||
testExec(
|
||||
t,
|
||||
ShortForm(fb, "one", "a", "two", "b", "three", "c", "four", "d"),
|
||||
"", "foo -ab true -cd true",
|
||||
"", "true;true;true;true",
|
||||
),
|
||||
)
|
||||
|
||||
t.Run("long", testExec(t, fb, "", "foo --one true", "", "true;false;false;false"))
|
||||
t.Run("long, false", testExec(t, fb, "", "foo --one false", "", "false;false;false;false"))
|
||||
t.Run("logn, with eq", testExec(t, fb, "", "foo --one=true", "", "true;false;false;false"))
|
||||
t.Run("long, mixed, first", testExec(t, fb, "", "foo --one false --two", "", "false;true;false;false"))
|
||||
t.Run("long, mixed, last", testExec(t, fb, "", "foo --one --two false", "", "true;false;false;false"))
|
||||
})
|
||||
|
||||
t.Run("expected bool option", func(t *testing.T) {
|
||||
t.Run("short, implicit", testExec(t, ShortForm(fb, "one", "a"), "", "foo -a", "", "true;false;false;false"))
|
||||
t.Run("short, explicit", testExec(t, ShortForm(fb, "one", "a"), "", "foo -a true", "", "true;false;false;false"))
|
||||
t.Run("short, automatic positional", testExec(t, ShortForm(fbp, "one", "a"), "", "foo -a bar", "", "true;false;false;false;bar"))
|
||||
t.Run("short, combined", testExec(t, ShortForm(fb, "one", "a", "two", "b"), "", "foo -ab true", "", "true;true;false;false"))
|
||||
t.Run(
|
||||
"short, combined, automatic positional",
|
||||
testExec(t, ShortForm(fbp, "one", "a", "two", "b"), "", "foo -ab bar", "", "true;true;false;false;bar"),
|
||||
)
|
||||
|
||||
t.Run("long, implicit", testExec(t, fb, "", "foo --one", "", "true;false;false;false"))
|
||||
t.Run("long, explicit", testExec(t, fb, "", "foo --one true", "", "true;false;false;false"))
|
||||
t.Run("long, automatic positional", testExec(t, fbp, "", "foo --one bar", "", "true;false;false;false;bar"))
|
||||
})
|
||||
|
||||
t.Run("positional", func(t *testing.T) {
|
||||
t.Run("basic", testExec(t, fp, "", "foo bar baz", "", "0;bar,baz"))
|
||||
t.Run("explicit", testExec(t, fp, "", "foo -- bar baz", "", "0;bar,baz"))
|
||||
t.Run("mixed", testExec(t, fp, "", "foo bar -- baz", "", "0;bar,baz"))
|
||||
t.Run("with option", testExec(t, fp, "", "foo bar --second-field 42 baz", "", "42;bar,baz"))
|
||||
t.Run("with bool option at the end", testExec(t, fbp, "", "foo bar baz --one", "", "true;false;false;false;bar;baz"))
|
||||
t.Run("with expected bool, implicit", testExec(t, fbp, "", "foo bar --one baz", "", "true;false;false;false;bar;baz"))
|
||||
t.Run("with expected bool, explicit", testExec(t, fbp, "", "foo bar --one true baz", "", "true;false;false;false;bar;baz"))
|
||||
t.Run("option format", testExec(t, fbp, "", "foo -- --one", "", "false;false;false;false;--one"))
|
||||
})
|
||||
|
||||
t.Run("example", func(t *testing.T) {
|
||||
type s struct {
|
||||
Foo bool
|
||||
Bar []bool
|
||||
Qux bool
|
||||
Quux string
|
||||
}
|
||||
|
||||
fs := func(s s, a1, a2 string) string {
|
||||
var sbar []string
|
||||
for _, b := range s.Bar {
|
||||
sbar = append(sbar, fmt.Sprint(b))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%t;%s;%t;%s;%s;%s", s.Foo, strings.Join(sbar, ","), s.Qux, s.Quux, a1, a2)
|
||||
}
|
||||
|
||||
t.Run(
|
||||
"full",
|
||||
testExec(
|
||||
t,
|
||||
ShortForm(fs, "foo", "a", "bar", "b"),
|
||||
"",
|
||||
"foo -ab --bar baz -b --qux --quux corge -- grault",
|
||||
"",
|
||||
"true;true,true,true;true;corge;baz;grault",
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("expected or unexpected", func(t *testing.T) {
|
||||
t.Run("capital letters", testExec(t, fp, "", "foo --One bar", "", "0;--One,bar"))
|
||||
t.Run("digit in option name", testExec(t, fd, "", "foo --one-2", "", "true"))
|
||||
t.Run("dash in option name", testExec(t, ff, "", "foo --second-field 42", "", "42"))
|
||||
t.Run("unpexpected character", testExec(t, fp, "", "foo --one#", "", "0;--one#"))
|
||||
t.Run(
|
||||
"invalid short option set",
|
||||
testExec(t, ShortForm(fp, "one", "a", "one", "b", "second-field", "c"), "", "foo -aBc", "", "0;-aBc"),
|
||||
)
|
||||
|
||||
t.Run("positional separator, no value", testExec(t, fp, "", "foo --one bar --", "", "bar0;"))
|
||||
t.Run("positional separator, expecting value", testExec(t, fp, "", "foo --one --", "--one", ""))
|
||||
t.Run("shot flag set, expecting value", testExec(t, ShortForm(fp, "second-field", "b"), "", "foo --one -b", "--one", ""))
|
||||
})
|
||||
|
||||
t.Run("preserve order", func(t *testing.T) {
|
||||
t.Run("bools", testExec(t, fl, "", "foo --one --one false --one", "", "true,false,true;"))
|
||||
t.Run("strings", testExec(t, fl, "", "foo --two 1 --two 2 --two 3", "", ";1,2,3"))
|
||||
})
|
||||
|
||||
t.Run("select subcommand", func(t *testing.T) {
|
||||
t.Run("named", testExec(t, Command("", nil, Command("bar", ff), Command("baz", ff)), "", "foo baz", "", "0"))
|
||||
t.Run("default", testExec(t, Command("", nil, Command("bar", ff), Default(Command("baz", ff))), "", "foo", "", "0"))
|
||||
})
|
||||
}
|
||||
70
env.go
Normal file
70
env.go
Normal file
@ -0,0 +1,70 @@
|
||||
package wand
|
||||
|
||||
import (
|
||||
"github.com/iancoleman/strcase"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type env struct {
|
||||
values map[string][]string
|
||||
originalNames map[string]string
|
||||
}
|
||||
|
||||
func splitEnvValue(v string) []string {
|
||||
var (
|
||||
values []string
|
||||
escape bool
|
||||
current []rune
|
||||
)
|
||||
|
||||
for _, r := range []rune(v) {
|
||||
if escape {
|
||||
current = append(current, r)
|
||||
escape = false
|
||||
continue
|
||||
}
|
||||
|
||||
if r == '\\' {
|
||||
escape = true
|
||||
continue
|
||||
}
|
||||
|
||||
if r == ':' {
|
||||
values = append(values, string(current))
|
||||
current = nil
|
||||
continue
|
||||
}
|
||||
|
||||
current = append(current, r)
|
||||
}
|
||||
|
||||
values = append(values, string(current))
|
||||
return values
|
||||
}
|
||||
|
||||
func readEnv(appName string, input []string) env {
|
||||
appName = strcase.ToKebab(appName)
|
||||
e := env{
|
||||
values: make(map[string][]string),
|
||||
originalNames: make(map[string]string),
|
||||
}
|
||||
|
||||
for _, i := range input {
|
||||
parts := strings.SplitN(i, "=", 2)
|
||||
if len(parts) != 2 {
|
||||
continue
|
||||
}
|
||||
|
||||
key, value := parts[0], parts[1]
|
||||
key = strcase.ToKebab(key)
|
||||
if len(key) <= len(appName)+1 || !strings.HasPrefix(key, appName+"-") {
|
||||
continue
|
||||
}
|
||||
|
||||
key = key[len(appName)+1:]
|
||||
e.originalNames[key] = parts[0]
|
||||
e.values[key] = splitEnvValue(value)
|
||||
}
|
||||
|
||||
return e
|
||||
}
|
||||
42
env_test.go
Normal file
42
env_test.go
Normal file
@ -0,0 +1,42 @@
|
||||
package wand
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEnv(t *testing.T) {
|
||||
type f struct{ One, SecondVar string }
|
||||
ff := func(f f) string {
|
||||
return f.One + f.SecondVar
|
||||
}
|
||||
|
||||
type i struct{ One, SecondVar int }
|
||||
fi := func(i i) string {
|
||||
return fmt.Sprintf("%d;%d", i.One, i.SecondVar)
|
||||
}
|
||||
|
||||
type m struct{ One, SecondVar []string }
|
||||
fm := func(m m) string {
|
||||
return strings.Join([]string{strings.Join(m.One, ","), strings.Join(m.SecondVar, ",")}, ";")
|
||||
}
|
||||
|
||||
t.Run("none match app prefix", testExec(t, ff, "SOME_VAR=foo;SOME_OTHER=bar", "baz", "", ""))
|
||||
t.Run("common environment var casing", testExec(t, ff, "FOO_ONE=bar;FOO_SECOND_VAR=baz", "foo", "", "barbaz"))
|
||||
t.Run("camel casing", testExec(t, ff, "fooOne=bar;fooSecondVar=baz", "foo", "", "barbaz"))
|
||||
t.Run("empty env var", testExec(t, ff, "fooOne=bar;fooSecondVar=", "foo", "", "bar"))
|
||||
t.Run("multipart app name", testExec(t, ff, "fooBarOne=baz;FOO_BAR_SECOND_VAR=qux", "foo-bar", "", "bazqux"))
|
||||
t.Run("invalid env var", testExec(t, ff, "fooOne=bar;fooSecondVar=baz;fooQux", "foo", "", "barbaz"))
|
||||
t.Run("eq in value", testExec(t, ff, "fooOne=bar=baz", "foo", "", "bar=baz"))
|
||||
t.Run("keeps original name", testExec(t, fi, "FOO_ONE=bar", "foo", "FOO_ONE", ""))
|
||||
t.Run("keeps original name, last wins on conflict", testExec(t, fi, "FOO_ONE=bar;fooOne=baz", "foo", "fooOne", ""))
|
||||
t.Run("multiple values", func(t *testing.T) {
|
||||
t.Run("2", testExec(t, fm, "fooOne=bar:baz", "foo", "", "bar,baz;"))
|
||||
t.Run("3", testExec(t, fm, "fooOne=bar:baz:qux", "foo", "", "bar,baz,qux;"))
|
||||
t.Run("with empty", testExec(t, fm, "fooOne=bar:baz::qux:", "foo", "", "bar,baz,,qux,;"))
|
||||
t.Run("escape", testExec(t, fm, "fooOne=bar\\:baz", "foo", "", "bar:baz;"))
|
||||
t.Run("escape char", testExec(t, fm, "fooOne=bar\\\\:baz", "foo", "", "bar\\,baz;"))
|
||||
t.Run("escape char last", testExec(t, fm, "fooOne=bar\\", "foo", "", "bar;"))
|
||||
})
|
||||
}
|
||||
60
exec.go
Normal file
60
exec.go
Normal file
@ -0,0 +1,60 @@
|
||||
package wand
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/iancoleman/strcase"
|
||||
"io"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func exec(stdout, stderr io.Writer, exit func(int), cmd Cmd, env, args []string) {
|
||||
cmd = insertHelp(cmd)
|
||||
_, cmd.name = filepath.Split(args[0])
|
||||
cmd.name = strcase.ToKebab(cmd.name)
|
||||
if err := validateCommand(cmd); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
args = args[1:]
|
||||
e := readEnv(cmd.name, env)
|
||||
cmd, fullCmd, args := selectCommand(cmd, args)
|
||||
if cmd.impl == nil {
|
||||
fmt.Fprint(stderr, errors.New("subcommand not specified"))
|
||||
suggestHelp(stderr, cmd, fullCmd)
|
||||
exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
if cmd.helpRequested {
|
||||
showHelp(stdout, cmd, fullCmd)
|
||||
return
|
||||
}
|
||||
|
||||
bo := boolOptions(cmd)
|
||||
cl := readArgs(bo, args)
|
||||
if hasHelpOption(cmd, cl.options) {
|
||||
showHelp(stdout, cmd, fullCmd)
|
||||
return
|
||||
}
|
||||
|
||||
if err := validateInput(cmd, e, cl); err != nil {
|
||||
fmt.Fprint(stderr, err)
|
||||
suggestHelp(stderr, cmd, fullCmd)
|
||||
exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
output, err := apply(cmd, e, cl)
|
||||
if err != nil {
|
||||
fmt.Fprint(stderr, err)
|
||||
exit(1)
|
||||
return
|
||||
}
|
||||
|
||||
if err := printOutput(stdout, output); err != nil {
|
||||
fmt.Fprint(stderr, err)
|
||||
exit(1)
|
||||
return
|
||||
}
|
||||
}
|
||||
45
exec_test.go
Normal file
45
exec_test.go
Normal file
@ -0,0 +1,45 @@
|
||||
package wand
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func testExec(impl any, env, commandLine, err string, expect ...string) func(*testing.T) {
|
||||
return func(t *testing.T) {
|
||||
var exitCode int
|
||||
exit := func(code int) { exitCode = code }
|
||||
stdout := bytes.NewBuffer(nil)
|
||||
stderr := bytes.NewBuffer(nil)
|
||||
cmd := wrap(impl)
|
||||
e := strings.Split(env, ";")
|
||||
a := strings.Split(commandLine, " ")
|
||||
exec(stdout, stderr, exit, cmd, e, a)
|
||||
if exitCode != 0 && err == "" {
|
||||
t.Fatal("non-zero exit code:", stderr.String())
|
||||
}
|
||||
|
||||
if err != "" && exitCode == 0 {
|
||||
t.Fatal("failed to fail")
|
||||
}
|
||||
|
||||
if err != "" && !strings.Contains(stderr.String(), err) {
|
||||
t.Fatal("expected error not received:", stderr.String())
|
||||
}
|
||||
|
||||
if exitCode != 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var expstr []string
|
||||
for _, e := range expect {
|
||||
expstr = append(expstr, fmt.Sprint(e))
|
||||
}
|
||||
|
||||
if stdout.String() != strings.Join(expstr, "\n")+"\n" {
|
||||
t.Fatal("unexpected output:", stdout.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
8
go.mod
Normal file
8
go.mod
Normal file
@ -0,0 +1,8 @@
|
||||
module code.squareroundforest.org/arpio/wand
|
||||
|
||||
go 1.24.2
|
||||
|
||||
require (
|
||||
code.squareroundforest.org/arpio/notation v0.0.0-20241225183158-af3bd591a174 // indirect
|
||||
github.com/iancoleman/strcase v0.3.0 // indirect
|
||||
)
|
||||
4
go.sum
Normal file
4
go.sum
Normal file
@ -0,0 +1,4 @@
|
||||
code.squareroundforest.org/arpio/notation v0.0.0-20241225183158-af3bd591a174 h1:DKMSagVY3uyRhJ4ohiwQzNnR6CWdVKLkg97A8eQGxQU=
|
||||
code.squareroundforest.org/arpio/notation v0.0.0-20241225183158-af3bd591a174/go.mod h1:ait4Fvg9o0+bq5hlxi9dAcPL5a+/sr33qsZPNpToMLY=
|
||||
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
|
||||
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
|
||||
81
help.go
Normal file
81
help.go
Normal file
@ -0,0 +1,81 @@
|
||||
package wand
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type (
|
||||
synopsis struct{}
|
||||
docOptions struct{}
|
||||
docArguments struct{}
|
||||
docSubcommands struct{}
|
||||
)
|
||||
|
||||
type doc struct {
|
||||
name string
|
||||
synopsis synopsis
|
||||
description string
|
||||
options docOptions
|
||||
arguments docArguments
|
||||
subcommands docSubcommands
|
||||
}
|
||||
|
||||
func help() Cmd {
|
||||
return Cmd{
|
||||
name: "help",
|
||||
isHelp: true,
|
||||
}
|
||||
}
|
||||
|
||||
func insertHelp(cmd Cmd) Cmd {
|
||||
var hasHelpCmd bool
|
||||
for i, sc := range cmd.subcommands {
|
||||
cmd.subcommands[i] = insertHelp(sc)
|
||||
if cmd.name == "help" {
|
||||
hasHelpCmd = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasHelpCmd {
|
||||
cmd.subcommands = append(cmd.subcommands, help())
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func hasHelpSubcommand(cmd Cmd) bool {
|
||||
for _, sc := range cmd.subcommands {
|
||||
if sc.isHelp {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func hasCustomHelpOption(cmd Cmd) bool {
|
||||
mf := mapFields(cmd.impl)
|
||||
_, has := mf["help"]
|
||||
return has
|
||||
}
|
||||
|
||||
func suggestHelp(out io.Writer, cmd Cmd, fullCommand []string) {
|
||||
if hasHelpSubcommand(cmd) {
|
||||
fmt.Fprintf(out, "Show help:\n%s help\n", strings.Join(fullCommand, " "))
|
||||
return
|
||||
}
|
||||
|
||||
if !hasCustomHelpOption(cmd) {
|
||||
fmt.Fprintf(out, "Show help:\n%s --help\n", strings.Join(fullCommand, " "))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func constructDoc(cmd Cmd, fullCommand []string) doc {
|
||||
return doc{}
|
||||
}
|
||||
|
||||
func showHelp(out io.Writer, cmd Cmd, fullCommand []string) {
|
||||
}
|
||||
166
input.go
Normal file
166
input.go
Normal file
@ -0,0 +1,166 @@
|
||||
package wand
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"slices"
|
||||
)
|
||||
|
||||
func validateEnv(cmd Cmd, e env) error {
|
||||
mf := mapFields(cmd.impl)
|
||||
for name, values := range e.values {
|
||||
f, ok := mf[name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, fi := range f {
|
||||
if len(values) > 1 && !fi.acceptsMultiple {
|
||||
return fmt.Errorf(
|
||||
"expected only one value, received %d, as environment value, %s",
|
||||
len(values),
|
||||
e.originalNames[name],
|
||||
)
|
||||
}
|
||||
|
||||
for _, v := range values {
|
||||
if !canScan(fi.typ, v) {
|
||||
return fmt.Errorf(
|
||||
"environment variable cannot be applied, type mismatch: %s",
|
||||
e.originalNames[name],
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateOptions(cmd Cmd, o []option) error {
|
||||
ml := make(map[string]string)
|
||||
ms := make(map[string]string)
|
||||
for i := 0; i < len(cmd.shortForms); i += 2 {
|
||||
l, s := cmd.shortForms[i], cmd.shortForms[i+1]
|
||||
ml[l] = s
|
||||
ms[s] = l
|
||||
}
|
||||
|
||||
mo := make(map[string][]option)
|
||||
for _, oi := range o {
|
||||
n := oi.name
|
||||
if ln, ok := ms[n]; ok && oi.shortForm {
|
||||
n = ln
|
||||
}
|
||||
|
||||
mo[n] = append(mo[n], oi)
|
||||
}
|
||||
|
||||
mf := mapFields(cmd.impl)
|
||||
for n, os := range mo {
|
||||
f := mf[n]
|
||||
for _, fi := range f {
|
||||
en := "--" + n
|
||||
if sn, ok := ml[n]; ok {
|
||||
en += ", -" + sn
|
||||
}
|
||||
|
||||
if len(os) > 1 && !fi.acceptsMultiple {
|
||||
return fmt.Errorf(
|
||||
"expected only one value, received %d, as option, %s",
|
||||
len(os),
|
||||
en,
|
||||
)
|
||||
}
|
||||
|
||||
for _, oi := range os {
|
||||
if oi.value.isBool && fi.typ.Kind() != reflect.Bool {
|
||||
return fmt.Errorf(
|
||||
"received boolean value for field that does not accept it: %s",
|
||||
en,
|
||||
)
|
||||
}
|
||||
|
||||
if !oi.value.isBool && !canScan(fi.typ, oi.value.str) {
|
||||
return fmt.Errorf(
|
||||
"option cannot be applied, type mismatch: %s",
|
||||
en,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validatePositionalArgs(cmd Cmd, a []string) error {
|
||||
v := reflect.ValueOf(cmd.impl)
|
||||
v = unpack(v)
|
||||
t := v.Type()
|
||||
p := positionalParameters(t)
|
||||
ior, iow := ioParameters(p)
|
||||
last := t.NumIn()-1
|
||||
lastVariadic := t.IsVariadic() &&
|
||||
!isStruct(t.In(last)) &&
|
||||
!slices.Contains(ior, last) &&
|
||||
!slices.Contains(iow, last)
|
||||
length := len(p) - len(ior) - len(iow)
|
||||
min := length
|
||||
max := length
|
||||
if lastVariadic {
|
||||
min--
|
||||
max = -1
|
||||
}
|
||||
|
||||
if cmd.minPositional > min {
|
||||
min = cmd.minPositional
|
||||
}
|
||||
|
||||
if cmd.maxPositional > 0 {
|
||||
max = cmd.maxPositional
|
||||
}
|
||||
|
||||
if len(a) < min {
|
||||
return fmt.Errorf("not enough positional arguments, expected minimum %d", min)
|
||||
}
|
||||
|
||||
if max >= 0 && len(a) > max {
|
||||
return fmt.Errorf("too many positional arguments, expected maximum %d", max)
|
||||
}
|
||||
|
||||
for i, ai := range a {
|
||||
var pi reflect.Type
|
||||
if i >= length {
|
||||
pi = p[length-1]
|
||||
} else {
|
||||
pi = p[i]
|
||||
}
|
||||
|
||||
if !canScan(pi, ai) {
|
||||
return fmt.Errorf(
|
||||
"cannot apply positional argument at index %d, expecting %v",
|
||||
i,
|
||||
pi,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateInput(cmd Cmd, e env, cl commandLine) error {
|
||||
if err := validateEnv(cmd, e); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validateOptions(cmd, cl.options); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := validatePositionalArgs(cmd, cl.positional); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
4
notes.txt
Normal file
4
notes.txt
Normal file
@ -0,0 +1,4 @@
|
||||
io.Writer arg: pass in os.Stdout
|
||||
io.Reader arg: pass in os.Stdin
|
||||
test: method docs
|
||||
during validation, reject circular type references
|
||||
25
output.go
Normal file
25
output.go
Normal file
@ -0,0 +1,25 @@
|
||||
package wand
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
func printOutput(w io.Writer, o []any) error {
|
||||
for _, oi := range o {
|
||||
r, ok := oi.(io.Reader)
|
||||
if ok {
|
||||
if _, err := io.Copy(w, r); err != nil {
|
||||
return fmt.Errorf("error copying output: %w", err)
|
||||
}
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
if _, err := fmt.Fprintf(w, "%v\n", oi); err != nil {
|
||||
return fmt.Errorf("error printing output: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
298
reflect.go
Normal file
298
reflect.go
Normal file
@ -0,0 +1,298 @@
|
||||
package wand
|
||||
|
||||
import (
|
||||
"github.com/iancoleman/strcase"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"io"
|
||||
)
|
||||
|
||||
type packedKind[T any] interface {
|
||||
Kind() reflect.Kind
|
||||
Elem() T
|
||||
}
|
||||
|
||||
type field struct {
|
||||
name string
|
||||
typ reflect.Type
|
||||
acceptsMultiple bool
|
||||
}
|
||||
|
||||
var (
|
||||
readerType = reflect.TypeFor[io.Reader]()
|
||||
writerType = reflect.TypeFor[io.Writer]()
|
||||
)
|
||||
|
||||
func pack(v reflect.Value, t reflect.Type) reflect.Value {
|
||||
if v.Type() == t {
|
||||
return v
|
||||
}
|
||||
|
||||
if t.Kind() == reflect.Pointer {
|
||||
pv := pack(v, t.Elem())
|
||||
p := reflect.New(t.Elem())
|
||||
p.Elem().Set(pv)
|
||||
return p
|
||||
}
|
||||
|
||||
iv := pack(v, t.Elem())
|
||||
s := reflect.MakeSlice(t, 1, 1)
|
||||
s.Index(0).Set(iv)
|
||||
return s
|
||||
}
|
||||
|
||||
func unpack[T packedKind[T]](p T) T {
|
||||
switch p.Kind() {
|
||||
case reflect.Pointer,
|
||||
reflect.Slice:
|
||||
return unpack(p.Elem())
|
||||
default:
|
||||
return p
|
||||
}
|
||||
}
|
||||
|
||||
func isReader(t reflect.Type) bool {
|
||||
return unpack(t) == readerType
|
||||
}
|
||||
|
||||
func isWriter(t reflect.Type) bool {
|
||||
return unpack(t) == writerType
|
||||
}
|
||||
|
||||
func isStruct(t reflect.Type) bool {
|
||||
t = unpack(t)
|
||||
return t.Kind() == reflect.Struct
|
||||
}
|
||||
|
||||
func canScan(t reflect.Type, s string) bool {
|
||||
switch t.Kind() {
|
||||
case reflect.Bool:
|
||||
_, err := strconv.ParseBool(s)
|
||||
return err == nil
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
_, err := strconv.ParseInt(s, 10, int(t.Size())*8)
|
||||
return err == nil
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
_, err := strconv.ParseUint(s, 10, int(t.Size())*8)
|
||||
return err == nil
|
||||
case reflect.Float32, reflect.Float64:
|
||||
_, err := strconv.ParseFloat(s, int(t.Size())*8)
|
||||
return err == nil
|
||||
case reflect.String:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func scan(t reflect.Type, s string) any {
|
||||
p := reflect.New(t)
|
||||
switch t.Kind() {
|
||||
case reflect.Bool:
|
||||
v, _ := strconv.ParseBool(s)
|
||||
p.Elem().Set(reflect.ValueOf(v).Convert(t))
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
v, _ := strconv.ParseInt(s, 10, int(t.Size())*8)
|
||||
p.Elem().Set(reflect.ValueOf(v).Convert(t))
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
v, _ := strconv.ParseUint(s, 10, int(t.Size())*8)
|
||||
p.Elem().Set(reflect.ValueOf(v).Convert(t))
|
||||
case reflect.Float32, reflect.Float64:
|
||||
v, _ := strconv.ParseFloat(s, int(t.Size())*8)
|
||||
p.Elem().Set(reflect.ValueOf(v).Convert(t))
|
||||
default:
|
||||
p.Elem().Set(reflect.ValueOf(s).Convert(t))
|
||||
}
|
||||
|
||||
return p.Elem().Interface()
|
||||
}
|
||||
|
||||
func fields(s ...reflect.Type) []field {
|
||||
if len(s) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
anonFields []field
|
||||
plainFields []field
|
||||
)
|
||||
|
||||
for i := 0; i < s[0].NumField(); i++ {
|
||||
sf := s[0].Field(i)
|
||||
sft := sf.Type
|
||||
am := acceptsMultiple(sft)
|
||||
sft = unpack(sft)
|
||||
sfn := sf.Name
|
||||
sfn = strcase.ToKebab(sfn)
|
||||
switch sft.Kind() {
|
||||
case reflect.Bool,
|
||||
reflect.Int,
|
||||
reflect.Int8,
|
||||
reflect.Int16,
|
||||
reflect.Int32,
|
||||
reflect.Int64,
|
||||
reflect.Uint,
|
||||
reflect.Uint8,
|
||||
reflect.Uint16,
|
||||
reflect.Uint32,
|
||||
reflect.Uint64,
|
||||
reflect.Float32,
|
||||
reflect.Float64,
|
||||
reflect.String:
|
||||
plainFields = append(plainFields, field{name: sfn, typ: sft, acceptsMultiple: am})
|
||||
case reflect.Interface:
|
||||
if sft.NumMethod() == 0 {
|
||||
plainFields = append(plainFields, field{name: sfn, typ: sft, acceptsMultiple: am})
|
||||
}
|
||||
case reflect.Struct:
|
||||
sff := fields(sft)
|
||||
if sf.Anonymous {
|
||||
anonFields = append(anonFields, sff...)
|
||||
} else {
|
||||
for i := range sff {
|
||||
sff[i].name = sfn + "-" + sff[i].name
|
||||
sff[i].acceptsMultiple = sff[i].acceptsMultiple || am
|
||||
}
|
||||
|
||||
plainFields = append(plainFields, sff...)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mf := make(map[string]field)
|
||||
for _, fi := range anonFields {
|
||||
mf[fi.name] = fi
|
||||
}
|
||||
|
||||
for _, fi := range plainFields {
|
||||
mf[fi.name] = fi
|
||||
}
|
||||
|
||||
var f []field
|
||||
for _, fi := range mf {
|
||||
f = append(f, fi)
|
||||
}
|
||||
|
||||
return append(f, fields(s[1:]...)...)
|
||||
}
|
||||
|
||||
func boolFields(f []field) []field {
|
||||
var b []field
|
||||
for _, fi := range f {
|
||||
if fi.typ.Kind() == reflect.Bool {
|
||||
b = append(b, fi)
|
||||
}
|
||||
}
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
func mapFields(impl any) map[string][]field {
|
||||
v := reflect.ValueOf(impl)
|
||||
t := v.Type()
|
||||
t = unpack(t)
|
||||
s := structParameters(t)
|
||||
f := fields(s...)
|
||||
mf := make(map[string][]field)
|
||||
for _, fi := range f {
|
||||
mf[fi.name] = append(mf[fi.name], fi)
|
||||
}
|
||||
|
||||
return mf
|
||||
}
|
||||
|
||||
func filterParameters(t reflect.Type, f func(reflect.Type) bool) []reflect.Type {
|
||||
var s []reflect.Type
|
||||
for i := 0; i < t.NumIn(); i++ {
|
||||
p := t.In(i)
|
||||
p = unpack(p)
|
||||
if f(p) {
|
||||
s = append(s, p)
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func positionalParameters(t reflect.Type) []reflect.Type {
|
||||
return filterParameters(t, func(p reflect.Type) bool {
|
||||
return p.Kind() != reflect.Struct
|
||||
})
|
||||
}
|
||||
|
||||
func ioParameters(p []reflect.Type) ([]int, []int) {
|
||||
var (
|
||||
reader []int
|
||||
writer []int
|
||||
)
|
||||
|
||||
for i, pi := range p {
|
||||
switch {
|
||||
case isReader(pi):
|
||||
reader = append(reader, i)
|
||||
case isWriter(pi):
|
||||
writer = append(writer, i)
|
||||
}
|
||||
}
|
||||
|
||||
return reader, writer
|
||||
}
|
||||
|
||||
func structParameters(t reflect.Type) []reflect.Type {
|
||||
return filterParameters(t, func(p reflect.Type) bool {
|
||||
return p.Kind() == reflect.Struct
|
||||
})
|
||||
}
|
||||
|
||||
func compatibleTypes(t ...reflect.Type) bool {
|
||||
if len(t) < 2 {
|
||||
return true
|
||||
}
|
||||
|
||||
t0 := t[0]
|
||||
t1 := t[1]
|
||||
switch t0.Kind() {
|
||||
case reflect.Bool:
|
||||
return t1.Kind() == reflect.Bool
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
switch t1.Kind() {
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
switch t1.Kind() {
|
||||
case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case reflect.Float32, reflect.Float64:
|
||||
switch t1.Kind() {
|
||||
case reflect.Float32, reflect.Float64:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
case reflect.String:
|
||||
return t1.Kind() == reflect.String
|
||||
case reflect.Interface:
|
||||
return t1.Kind() == reflect.Interface && t0.NumMethod() == 0 && t1.NumMethod() == 0
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func acceptsMultiple(t reflect.Type) bool {
|
||||
if t.Kind() == reflect.Slice {
|
||||
return true
|
||||
}
|
||||
|
||||
switch t.Kind() {
|
||||
case reflect.Pointer, reflect.Slice:
|
||||
return acceptsMultiple(t.Elem())
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
42
wand.go
Normal file
42
wand.go
Normal file
@ -0,0 +1,42 @@
|
||||
package wand
|
||||
|
||||
import "os"
|
||||
|
||||
type Cmd struct {
|
||||
name string
|
||||
impl any
|
||||
subcommands []Cmd
|
||||
isDefault bool
|
||||
minPositional int
|
||||
maxPositional int
|
||||
shortForms []string
|
||||
description string
|
||||
isHelp bool
|
||||
helpRequested bool
|
||||
}
|
||||
|
||||
func Command(name string, impl any, subcmds ...Cmd) Cmd {
|
||||
return command(name, impl, subcmds...)
|
||||
}
|
||||
|
||||
func Default(cmd Cmd) Cmd {
|
||||
cmd.isDefault = true
|
||||
return cmd
|
||||
}
|
||||
|
||||
// io doesn't count
|
||||
func Args(cmd Cmd, min, max int) Cmd {
|
||||
cmd.minPositional = min
|
||||
cmd.maxPositional = max
|
||||
return cmd
|
||||
}
|
||||
|
||||
func ShortForm(cmd Cmd, f ...string) Cmd {
|
||||
cmd.shortForms = f
|
||||
return cmd
|
||||
}
|
||||
|
||||
func Exec(impl any) {
|
||||
cmd := wrap(impl)
|
||||
exec(os.Stdout, os.Stderr, os.Exit, cmd, os.Environ(), os.Args)
|
||||
}
|
||||
1
wand_test.go
Normal file
1
wand_test.go
Normal file
@ -0,0 +1 @@
|
||||
package wand
|
||||
Loading…
Reference in New Issue
Block a user