init repo

This commit is contained in:
Arpad Ryszka 2025-08-18 14:24:31 +02:00
commit 0131d6d955
20 changed files with 2408 additions and 0 deletions

457
.cover Normal file
View 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
View 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
View 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
View File

@ -0,0 +1,12 @@
package main
// myFunc is.
func myFunc() {
}
// MyFunc is.
func MyFunc() {
}
func main() {
}

236
command.go Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View File

@ -0,0 +1 @@
package wand