mirror of https://github.com/containers/podman.git
commit
738d62ea96
|
|
@ -219,6 +219,13 @@ The [username[:password]] to use to authenticate with the registry if required.
|
|||
If one or both values are not supplied, a command line prompt will appear and
|
||||
the value can be entered. The password is entered without echo.
|
||||
|
||||
#### **--decryption-key**=*key[:passphrase]*
|
||||
|
||||
The [key[:passphrase]] to be used for decryption of images. Key can point to
|
||||
keys and/or certificates. Decryption will be tried with all keys. If the key is
|
||||
protected by a passphrase, it is required to be passed in the argument and
|
||||
omitted otherwise.
|
||||
|
||||
#### **--device**=_host-device_[**:**_container-device_][**:**_permissions_]
|
||||
|
||||
Add a host device to the container. Optional *permissions* parameter
|
||||
|
|
|
|||
4
go.mod
4
go.mod
|
|
@ -10,10 +10,10 @@ require (
|
|||
github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd // indirect
|
||||
github.com/containernetworking/cni v0.8.0
|
||||
github.com/containernetworking/plugins v0.8.7
|
||||
github.com/containers/buildah v1.17.0
|
||||
github.com/containers/buildah v1.17.1-0.20201113135631-d0c958d65eb2
|
||||
github.com/containers/common v0.27.0
|
||||
github.com/containers/conmon v2.0.20+incompatible
|
||||
github.com/containers/image/v5 v5.7.0
|
||||
github.com/containers/image/v5 v5.8.0
|
||||
github.com/containers/psgo v1.5.1
|
||||
github.com/containers/storage v1.23.9
|
||||
github.com/coreos/go-systemd/v22 v22.1.0
|
||||
|
|
|
|||
28
go.sum
28
go.sum
|
|
@ -59,6 +59,12 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf
|
|||
github.com/checkpoint-restore/go-criu v0.0.0-20190109184317-bdb7599cd87b h1:T4nWG1TXIxeor8mAu5bFguPJgSIGhZqv/f0z55KCrJM=
|
||||
github.com/checkpoint-restore/go-criu v0.0.0-20190109184317-bdb7599cd87b/go.mod h1:TrMrLQfeENAPYPRsJuq3jsqdlRh3lvi6trTZJG8+tho=
|
||||
github.com/checkpoint-restore/go-criu/v4 v4.0.2/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
|
||||
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cilium/ebpf v0.0.0-20200507155900-a9f01edf17e3/go.mod h1:XT+cAw5wfvsodedcijoh1l9cf7v1x9FlFB/3VmF/O8s=
|
||||
github.com/cilium/ebpf v0.0.0-20200702112145-1c8d4c9ef775/go.mod h1:7cR51M8ViRLIdUjrmSXlK9pkrsDlLHbO8jiB8X8JnOc=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
|
|
@ -85,15 +91,16 @@ github.com/containernetworking/cni v0.8.0 h1:BT9lpgGoH4jw3lFC7Odz2prU5ruiYKcgAjM
|
|||
github.com/containernetworking/cni v0.8.0/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
|
||||
github.com/containernetworking/plugins v0.8.7 h1:bU7QieuAp+sACI2vCzESJ3FoT860urYP+lThyZkb/2M=
|
||||
github.com/containernetworking/plugins v0.8.7/go.mod h1:R7lXeZaBzpfqapcAbHRW8/CYwm0dHzbz0XEjofx0uB0=
|
||||
github.com/containers/buildah v1.17.0 h1:oaBIxKtW4kJ06vj4l0C9MZfFVapksf6F4qdQGOvZ2J4=
|
||||
github.com/containers/buildah v1.17.0/go.mod h1:E6nOiMnF3uCAY3wAQK5lPR6w89SRp8iyIkjUfDKW+Eg=
|
||||
github.com/containers/common v0.26.2/go.mod h1:igUeog5hx8rYhJk67rG6rGAh3zEcf0Uxuzm9KpXzo2E=
|
||||
github.com/containers/buildah v1.17.1-0.20201113135631-d0c958d65eb2 h1:sYOJ4xbCJTQEhjQax649sE+iy8ZohxmLGP8pCTrnypY=
|
||||
github.com/containers/buildah v1.17.1-0.20201113135631-d0c958d65eb2/go.mod h1:+GBrGojiBt2/IXxKYMCVD02kLIxfe5KYMvCwBjhJkFk=
|
||||
github.com/containers/common v0.26.3/go.mod h1:hJWZIlrl5MsE2ELNRa+MPp6I1kPbXHauuj0Ym4BsLG4=
|
||||
github.com/containers/common v0.27.0 h1:+QlYEOitVYtU9/x8xebRgxdGqt4sLaIqV6MBOns+zLk=
|
||||
github.com/containers/common v0.27.0/go.mod h1:ZTswJJfu4aGF6Anyi2yON8Getda9NDYcdIzurOEHHXI=
|
||||
github.com/containers/conmon v2.0.20+incompatible h1:YbCVSFSCqFjjVwHTPINGdMX1F6JXHGTUje2ZYobNrkg=
|
||||
github.com/containers/conmon v2.0.20+incompatible/go.mod h1:hgwZ2mtuDrppv78a/cOBNiCm6O0UMWGx1mu7P00nu5I=
|
||||
github.com/containers/image/v5 v5.7.0 h1:fiTC8/Xbr+zEP6njGTZtPW/3UD7MC93nC9DbUoWdxkA=
|
||||
github.com/containers/image/v5 v5.7.0/go.mod h1:8aOy+YaItukxghRORkvhq5ibWttHErzDLy6egrKfKos=
|
||||
github.com/containers/image/v5 v5.8.0 h1:B3FGHi0bdGXgg698kBIGOlHCXN5n+scJr6/5354GOPU=
|
||||
github.com/containers/image/v5 v5.8.0/go.mod h1:jKxdRtyIDumVa56hdsZvV+gwx4zB50hRou6pIuCWLkg=
|
||||
github.com/containers/libtrust v0.0.0-20190913040956-14b96171aa3b h1:Q8ePgVfHDplZ7U33NwHZkrVELsZP5fYj9pM5WBZB2GE=
|
||||
github.com/containers/libtrust v0.0.0-20190913040956-14b96171aa3b/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY=
|
||||
github.com/containers/ocicrypt v1.0.3 h1:vYgl+RZ9Q3DPMuTfxmN+qp0X2Bj52uuY2vnt6GzVe1c=
|
||||
|
|
@ -101,7 +108,6 @@ github.com/containers/ocicrypt v1.0.3/go.mod h1:CUBa+8MRNL/VkpxYIpaMtgn1WgXGyvPQ
|
|||
github.com/containers/psgo v1.5.1 h1:MQNb7FLbXqBdqz6u4lI2QWizVz4RSTzs1+Nk9XT1iVA=
|
||||
github.com/containers/psgo v1.5.1/go.mod h1:2ubh0SsreMZjSXW1Hif58JrEcFudQyIy9EzPUWfawVU=
|
||||
github.com/containers/storage v1.23.6/go.mod h1:haFs0HRowKwyzvWEx9EgI3WsL8XCSnBDb5f8P5CAxJY=
|
||||
github.com/containers/storage v1.23.7 h1:43ImvG/npvQSZXRjaudVvKISIuZSfI6qvtSNQQSGO/A=
|
||||
github.com/containers/storage v1.23.7/go.mod h1:cUT2zHjtx+WlVri30obWmM2gpqpi8jfPsmIzP1TVpEI=
|
||||
github.com/containers/storage v1.23.9 h1:qbgnTp76pLSyW3vYwY5GH4vk5cHYVXFJ+CsUEBp9TMw=
|
||||
github.com/containers/storage v1.23.9/go.mod h1:3b2ktpB6pw53SEeIoFfO0sQfP9+IoJJKPq5iJk74gxE=
|
||||
|
|
@ -303,17 +309,19 @@ github.com/json-iterator/go v1.1.10 h1:Kz6Cvnvv2wGdaG/V8yMvfkmNiXq9Ya2KUv4rouJJr
|
|||
github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=
|
||||
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.11.1 h1:bPb7nMRdOZYDrpPMTA3EInUQrdgoBinqUuSwlGdKDdE=
|
||||
github.com/klauspost/compress v1.11.1/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.11.2 h1:MiK62aErc3gIiVEtyzKfeOHgW7atJb5g/KNX5m3c2nQ=
|
||||
github.com/klauspost/compress v1.11.2/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
|
||||
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
|
|
@ -322,10 +330,17 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw=
|
||||
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo=
|
||||
github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ=
|
||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-shellwords v1.0.3/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o=
|
||||
|
|
@ -633,6 +648,7 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h
|
|||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import (
|
|||
"github.com/containers/image/v5/manifest"
|
||||
ociarchive "github.com/containers/image/v5/oci/archive"
|
||||
"github.com/containers/image/v5/oci/layout"
|
||||
"github.com/containers/image/v5/pkg/shortnames"
|
||||
is "github.com/containers/image/v5/storage"
|
||||
"github.com/containers/image/v5/tarball"
|
||||
"github.com/containers/image/v5/transports"
|
||||
|
|
@ -164,7 +165,7 @@ func (ir *Runtime) New(ctx context.Context, name, signaturePolicyPath, authfile
|
|||
}
|
||||
imageName, err := ir.pullImageFromHeuristicSource(ctx, name, writer, authfile, signaturePolicyPath, signingoptions, dockeroptions, &retry.RetryOptions{MaxRetry: maxRetry}, label)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "unable to pull %s", name)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newImage, err := ir.NewFromLocal(imageName[0])
|
||||
|
|
@ -318,10 +319,8 @@ func (ir *Runtime) LoadAllImagesFromDockerArchive(ctx context.Context, fileName
|
|||
}
|
||||
|
||||
goal := pullGoal{
|
||||
pullAllPairs: true,
|
||||
usedSearchRegistries: false,
|
||||
refPairs: refPairs,
|
||||
searchedRegistries: nil,
|
||||
pullAllPairs: true,
|
||||
refPairs: refPairs,
|
||||
}
|
||||
|
||||
defer goal.cleanUp()
|
||||
|
|
@ -456,22 +455,19 @@ func (ir *Runtime) getLocalImage(inputName string) (string, *storage.Image, erro
|
|||
return "", nil, errors.Wrapf(ErrNoSuchImage, imageError)
|
||||
}
|
||||
|
||||
// "Short-name image", so let's try out certain prefixes:
|
||||
// 1) DefaultLocalRegistry (i.e., "localhost/)
|
||||
// 2) Unqualified-search registries from registries.conf
|
||||
unqualifiedSearchRegistries, err := registries.GetRegistries()
|
||||
sys := &types.SystemContext{
|
||||
SystemRegistriesConfPath: registries.SystemRegistriesConfPath(),
|
||||
}
|
||||
|
||||
candidates, err := shortnames.ResolveLocally(sys, inputName)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
for _, candidate := range append([]string{DefaultLocalRegistry}, unqualifiedSearchRegistries...) {
|
||||
ref, err := decomposedImage.referenceWithRegistry(candidate)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
img, err := ir.store.Image(reference.TagNameOnly(ref).String())
|
||||
for _, candidate := range candidates {
|
||||
img, err := ir.store.Image(candidate.String())
|
||||
if err == nil {
|
||||
return ref.String(), img, nil
|
||||
return candidate.String(), img, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
|
|
@ -15,13 +16,14 @@ import (
|
|||
dockerarchive "github.com/containers/image/v5/docker/archive"
|
||||
ociarchive "github.com/containers/image/v5/oci/archive"
|
||||
oci "github.com/containers/image/v5/oci/layout"
|
||||
"github.com/containers/image/v5/pkg/shortnames"
|
||||
is "github.com/containers/image/v5/storage"
|
||||
"github.com/containers/image/v5/transports"
|
||||
"github.com/containers/image/v5/transports/alltransports"
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/containers/podman/v2/libpod/events"
|
||||
"github.com/containers/podman/v2/pkg/errorhandling"
|
||||
"github.com/containers/podman/v2/pkg/registries"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/opentracing/opentracing-go"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/sirupsen/logrus"
|
||||
|
|
@ -56,9 +58,10 @@ var (
|
|||
|
||||
// pullRefPair records a pair of prepared image references to pull.
|
||||
type pullRefPair struct {
|
||||
image string
|
||||
srcRef types.ImageReference
|
||||
dstRef types.ImageReference
|
||||
image string
|
||||
srcRef types.ImageReference
|
||||
dstRef types.ImageReference
|
||||
resolvedShortname *shortnames.PullCandidate // if set, must be recorded after successful pull
|
||||
}
|
||||
|
||||
// cleanUpFunc is a function prototype for clean-up functions.
|
||||
|
|
@ -66,11 +69,11 @@ type cleanUpFunc func() error
|
|||
|
||||
// pullGoal represents the prepared image references and decided behavior to be executed by imagePull
|
||||
type pullGoal struct {
|
||||
refPairs []pullRefPair
|
||||
pullAllPairs bool // Pull all refPairs instead of stopping on first success.
|
||||
usedSearchRegistries bool // refPairs construction has depended on registries.GetRegistries()
|
||||
searchedRegistries []string // The list of search registries used; set only if usedSearchRegistries
|
||||
cleanUpFuncs []cleanUpFunc // Mainly used to close long-lived objects (e.g., an archive.Reader)
|
||||
refPairs []pullRefPair
|
||||
pullAllPairs bool // Pull all refPairs instead of stopping on first success.
|
||||
cleanUpFuncs []cleanUpFunc // Mainly used to close long-lived objects (e.g., an archive.Reader)
|
||||
shortName string // Set when pulling a short name
|
||||
resolved *shortnames.Resolved // Set when pulling a short name
|
||||
}
|
||||
|
||||
// cleanUp invokes all cleanUpFuncs. Certain resources may not be available
|
||||
|
|
@ -86,10 +89,8 @@ func (p *pullGoal) cleanUp() {
|
|||
// singlePullRefPairGoal returns a no-frills pull goal for the specified reference pair.
|
||||
func singlePullRefPairGoal(rp pullRefPair) *pullGoal {
|
||||
return &pullGoal{
|
||||
refPairs: []pullRefPair{rp},
|
||||
pullAllPairs: false, // Does not really make a difference.
|
||||
usedSearchRegistries: false,
|
||||
searchedRegistries: nil,
|
||||
refPairs: []pullRefPair{rp},
|
||||
pullAllPairs: false, // Does not really make a difference.
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -193,11 +194,9 @@ func (ir *Runtime) pullGoalFromImageReference(ctx context.Context, srcRef types.
|
|||
}
|
||||
|
||||
return &pullGoal{
|
||||
pullAllPairs: true,
|
||||
usedSearchRegistries: false,
|
||||
refPairs: pairs,
|
||||
searchedRegistries: nil,
|
||||
cleanUpFuncs: []cleanUpFunc{reader.Close},
|
||||
pullAllPairs: true,
|
||||
refPairs: pairs,
|
||||
cleanUpFuncs: []cleanUpFunc{reader.Close},
|
||||
}, nil
|
||||
|
||||
case OCIArchive:
|
||||
|
|
@ -267,7 +266,7 @@ func (ir *Runtime) pullImageFromHeuristicSource(ctx context.Context, inputName s
|
|||
if srcTransport != nil && srcTransport.Name() != DockerTransport {
|
||||
return nil, err
|
||||
}
|
||||
goal, err = ir.pullGoalFromPossiblyUnqualifiedName(inputName)
|
||||
goal, err = ir.pullGoalFromPossiblyUnqualifiedName(sc, writer, inputName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error getting default registries to try")
|
||||
}
|
||||
|
|
@ -325,7 +324,7 @@ func (ir *Runtime) doPullImage(ctx context.Context, sc *types.SystemContext, goa
|
|||
|
||||
var (
|
||||
images []string
|
||||
pullErrors *multierror.Error
|
||||
pullErrors []error
|
||||
)
|
||||
|
||||
for _, imageInfo := range goal.refPairs {
|
||||
|
|
@ -348,12 +347,17 @@ func (ir *Runtime) doPullImage(ctx context.Context, sc *types.SystemContext, goa
|
|||
_, err = cp.Image(ctx, policyContext, imageInfo.dstRef, imageInfo.srcRef, copyOptions)
|
||||
return err
|
||||
}, retryOptions); err != nil {
|
||||
pullErrors = multierror.Append(pullErrors, err)
|
||||
pullErrors = append(pullErrors, err)
|
||||
logrus.Debugf("Error pulling image ref %s: %v", imageInfo.srcRef.StringWithinTransport(), err)
|
||||
if writer != nil {
|
||||
_, _ = io.WriteString(writer, cleanErrorMessage(err))
|
||||
}
|
||||
} else {
|
||||
if imageInfo.resolvedShortname != nil {
|
||||
if err := imageInfo.resolvedShortname.Record(); err != nil {
|
||||
logrus.Errorf("Error recording short-name alias %q: %v", imageInfo.resolvedShortname.Value.String(), err)
|
||||
}
|
||||
}
|
||||
if !goal.pullAllPairs {
|
||||
ir.newImageEvent(events.Pull, "")
|
||||
return []string{imageInfo.image}, nil
|
||||
|
|
@ -361,68 +365,75 @@ func (ir *Runtime) doPullImage(ctx context.Context, sc *types.SystemContext, goa
|
|||
images = append(images, imageInfo.image)
|
||||
}
|
||||
}
|
||||
// If no image was found, we should handle. Lets be nicer to the user and see if we can figure out why.
|
||||
// If no image was found, we should handle. Lets be nicer to the user
|
||||
// and see if we can figure out why.
|
||||
if len(images) == 0 {
|
||||
if goal.usedSearchRegistries && len(goal.searchedRegistries) == 0 {
|
||||
return nil, errors.Errorf("image name provided is a short name and no search registries are defined in the registries config file.")
|
||||
if goal.resolved != nil {
|
||||
return nil, goal.resolved.FormatPullErrors(pullErrors)
|
||||
}
|
||||
// If the image passed in was fully-qualified, we will have 1 refpair. Bc the image is fq'd, we don't need to yap about registries.
|
||||
if !goal.usedSearchRegistries {
|
||||
if pullErrors != nil && len(pullErrors.Errors) > 0 { // this should always be true
|
||||
return nil, pullErrors.Errors[0]
|
||||
}
|
||||
return nil, errors.Errorf("unable to pull image, or you do not have pull access")
|
||||
}
|
||||
return nil, errors.Cause(pullErrors)
|
||||
}
|
||||
if len(images) > 0 {
|
||||
ir.newImageEvent(events.Pull, images[0])
|
||||
return nil, errorhandling.JoinErrors(pullErrors)
|
||||
}
|
||||
|
||||
ir.newImageEvent(events.Pull, images[0])
|
||||
return images, nil
|
||||
}
|
||||
|
||||
// getShortNameMode looks up the `CONTAINERS_SHORT_NAME_ALIASING` environment
|
||||
// variable. If it's "on", return `nil` to use the defaults from
|
||||
// containers/image and the registries.conf files on the system. If it's
|
||||
// "off", empty or unset, return types.ShortNameModeDisabled to turn off
|
||||
// short-name aliasing by default.
|
||||
//
|
||||
// TODO: remove this function once we want to default to short-name aliasing.
|
||||
func getShortNameMode() *types.ShortNameMode {
|
||||
env := os.Getenv("CONTAINERS_SHORT_NAME_ALIASING")
|
||||
if strings.ToLower(env) == "on" {
|
||||
return nil // default to whatever registries.conf and c/image decide
|
||||
}
|
||||
mode := types.ShortNameModeDisabled
|
||||
return &mode
|
||||
}
|
||||
|
||||
// pullGoalFromPossiblyUnqualifiedName looks at inputName and determines the possible
|
||||
// image references to try pulling in combination with the registries.conf file as well
|
||||
func (ir *Runtime) pullGoalFromPossiblyUnqualifiedName(inputName string) (*pullGoal, error) {
|
||||
decomposedImage, err := decompose(inputName)
|
||||
func (ir *Runtime) pullGoalFromPossiblyUnqualifiedName(sys *types.SystemContext, writer io.Writer, inputName string) (*pullGoal, error) {
|
||||
if sys == nil {
|
||||
sys = &types.SystemContext{}
|
||||
}
|
||||
sys.ShortNameMode = getShortNameMode()
|
||||
|
||||
resolved, err := shortnames.Resolve(sys, inputName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if decomposedImage.hasRegistry {
|
||||
srcRef, err := docker.ParseReference("//" + inputName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "unable to parse '%s'", inputName)
|
||||
if desc := resolved.Description(); len(desc) > 0 {
|
||||
logrus.Debug(desc)
|
||||
if writer != nil {
|
||||
if _, err := writer.Write([]byte(desc + "\n")); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return ir.getSinglePullRefPairGoal(srcRef, inputName)
|
||||
}
|
||||
|
||||
searchRegistries, err := registries.GetRegistries()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
refPairs := make([]pullRefPair, 0, len(searchRegistries))
|
||||
for _, registry := range searchRegistries {
|
||||
ref, err := decomposedImage.referenceWithRegistry(registry)
|
||||
refPairs := []pullRefPair{}
|
||||
for i, candidate := range resolved.PullCandidates {
|
||||
srcRef, err := docker.NewReference(candidate.Value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
imageName := ref.String()
|
||||
srcRef, err := docker.ParseReference("//" + imageName)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "unable to parse '%s'", imageName)
|
||||
}
|
||||
ps, err := ir.getPullRefPair(srcRef, imageName)
|
||||
ps, err := ir.getPullRefPair(srcRef, candidate.Value.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ps.resolvedShortname = &resolved.PullCandidates[i]
|
||||
refPairs = append(refPairs, ps)
|
||||
}
|
||||
return &pullGoal{
|
||||
refPairs: refPairs,
|
||||
pullAllPairs: false,
|
||||
usedSearchRegistries: true,
|
||||
searchedRegistries: searchRegistries,
|
||||
refPairs: refPairs,
|
||||
pullAllPairs: false,
|
||||
shortName: inputName,
|
||||
resolved: resolved,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -278,15 +278,11 @@ func TestPullGoalFromImageReference(t *testing.T) {
|
|||
assert.Equal(t, e.dstName, storageReferenceWithoutLocation(res.refPairs[i].dstRef), testDescription)
|
||||
}
|
||||
assert.Equal(t, c.expectedPullAllPairs, res.pullAllPairs, c.srcName)
|
||||
assert.False(t, res.usedSearchRegistries, c.srcName)
|
||||
assert.Nil(t, res.searchedRegistries, c.srcName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const registriesConfWithSearch = `[registries.search]
|
||||
registries = ['example.com', 'docker.io']
|
||||
`
|
||||
const registriesConfWithSearch = `unqualified-search-registries = ['example.com', 'docker.io']`
|
||||
|
||||
func TestPullGoalFromPossiblyUnqualifiedName(t *testing.T) {
|
||||
const digestSuffix = "@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"
|
||||
|
|
@ -303,69 +299,58 @@ func TestPullGoalFromPossiblyUnqualifiedName(t *testing.T) {
|
|||
ir, cleanup := newTestRuntime(t)
|
||||
defer cleanup()
|
||||
|
||||
// Environment is per-process, so this looks very unsafe; actually it seems fine because tests are not
|
||||
// run in parallel unless they opt in by calling t.Parallel(). So don’t do that.
|
||||
oldRCP, hasRCP := os.LookupEnv("REGISTRIES_CONFIG_PATH")
|
||||
defer func() {
|
||||
if hasRCP {
|
||||
os.Setenv("REGISTRIES_CONFIG_PATH", oldRCP)
|
||||
} else {
|
||||
os.Unsetenv("REGISTRIES_CONFIG_PATH")
|
||||
}
|
||||
}()
|
||||
os.Setenv("REGISTRIES_CONFIG_PATH", registriesConf.Name())
|
||||
sc := GetSystemContext("", "", false)
|
||||
|
||||
aliasesConf, err := ioutil.TempFile("", "short-name-aliases.conf")
|
||||
require.NoError(t, err)
|
||||
defer aliasesConf.Close()
|
||||
defer os.Remove(aliasesConf.Name())
|
||||
sc.UserShortNameAliasConfPath = aliasesConf.Name()
|
||||
sc.SystemRegistriesConfPath = registriesConf.Name()
|
||||
|
||||
for _, c := range []struct {
|
||||
input string
|
||||
expected []pullRefStrings
|
||||
expectedUsedSearchRegistries bool
|
||||
input string
|
||||
expected []pullRefStrings
|
||||
}{
|
||||
{"#", nil, false}, // Clearly invalid.
|
||||
{"#", nil}, // Clearly invalid.
|
||||
{ // Fully-explicit docker.io, name-only.
|
||||
"docker.io/library/busybox",
|
||||
// (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".)
|
||||
[]pullRefStrings{{"docker.io/library/busybox", "docker://busybox:latest", "docker.io/library/busybox:latest"}},
|
||||
false,
|
||||
[]pullRefStrings{{"docker.io/library/busybox:latest", "docker://busybox:latest", "docker.io/library/busybox:latest"}},
|
||||
},
|
||||
{ // docker.io with implied /library/, name-only.
|
||||
"docker.io/busybox",
|
||||
// (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".)
|
||||
[]pullRefStrings{{"docker.io/busybox", "docker://busybox:latest", "docker.io/library/busybox:latest"}},
|
||||
false,
|
||||
[]pullRefStrings{{"docker.io/library/busybox:latest", "docker://busybox:latest", "docker.io/library/busybox:latest"}},
|
||||
},
|
||||
{ // Qualified example.com, name-only.
|
||||
"example.com/ns/busybox",
|
||||
[]pullRefStrings{{"example.com/ns/busybox", "docker://example.com/ns/busybox:latest", "example.com/ns/busybox:latest"}},
|
||||
false,
|
||||
[]pullRefStrings{{"example.com/ns/busybox:latest", "docker://example.com/ns/busybox:latest", "example.com/ns/busybox:latest"}},
|
||||
},
|
||||
{ // Qualified example.com, name:tag.
|
||||
"example.com/ns/busybox:notlatest",
|
||||
[]pullRefStrings{{"example.com/ns/busybox:notlatest", "docker://example.com/ns/busybox:notlatest", "example.com/ns/busybox:notlatest"}},
|
||||
false,
|
||||
},
|
||||
{ // Qualified example.com, name@digest.
|
||||
"example.com/ns/busybox" + digestSuffix,
|
||||
[]pullRefStrings{{"example.com/ns/busybox" + digestSuffix, "docker://example.com/ns/busybox" + digestSuffix,
|
||||
"example.com/ns/busybox" + digestSuffix}},
|
||||
false,
|
||||
},
|
||||
// Qualified example.com, name:tag@digest. This code is happy to try, but .srcRef parsing currently rejects such input.
|
||||
{"example.com/ns/busybox:notlatest" + digestSuffix, nil, false},
|
||||
{"example.com/ns/busybox:notlatest" + digestSuffix, nil},
|
||||
{ // Unqualified, single-name, name-only
|
||||
"busybox",
|
||||
[]pullRefStrings{
|
||||
{"example.com/busybox", "docker://example.com/busybox:latest", "example.com/busybox:latest"},
|
||||
{"example.com/busybox:latest", "docker://example.com/busybox:latest", "example.com/busybox:latest"},
|
||||
// (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".)
|
||||
{"docker.io/library/busybox", "docker://busybox:latest", "docker.io/library/busybox:latest"},
|
||||
{"docker.io/library/busybox:latest", "docker://busybox:latest", "docker.io/library/busybox:latest"},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{ // Unqualified, namespaced, name-only
|
||||
"ns/busybox",
|
||||
[]pullRefStrings{
|
||||
{"example.com/ns/busybox", "docker://example.com/ns/busybox:latest", "example.com/ns/busybox:latest"},
|
||||
{"example.com/ns/busybox:latest", "docker://example.com/ns/busybox:latest", "example.com/ns/busybox:latest"},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{ // Unqualified, name:tag
|
||||
"busybox:notlatest",
|
||||
|
|
@ -374,7 +359,6 @@ func TestPullGoalFromPossiblyUnqualifiedName(t *testing.T) {
|
|||
// (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".)
|
||||
{"docker.io/library/busybox:notlatest", "docker://busybox:notlatest", "docker.io/library/busybox:notlatest"},
|
||||
},
|
||||
true,
|
||||
},
|
||||
{ // Unqualified, name@digest
|
||||
"busybox" + digestSuffix,
|
||||
|
|
@ -383,29 +367,22 @@ func TestPullGoalFromPossiblyUnqualifiedName(t *testing.T) {
|
|||
// (The docker:// representation is shortened by c/image/docker.Reference but it refers to "docker.io/library".)
|
||||
{"docker.io/library/busybox" + digestSuffix, "docker://busybox" + digestSuffix, "docker.io/library/busybox" + digestSuffix},
|
||||
},
|
||||
true,
|
||||
},
|
||||
// Unqualified, name:tag@digest. This code is happy to try, but .srcRef parsing currently rejects such input.
|
||||
{"busybox:notlatest" + digestSuffix, nil, false},
|
||||
{"busybox:notlatest" + digestSuffix, nil},
|
||||
} {
|
||||
res, err := ir.pullGoalFromPossiblyUnqualifiedName(c.input)
|
||||
res, err := ir.pullGoalFromPossiblyUnqualifiedName(sc, nil, c.input)
|
||||
if len(c.expected) == 0 {
|
||||
assert.Error(t, err, c.input)
|
||||
} else {
|
||||
assert.NoError(t, err, c.input)
|
||||
for i, e := range c.expected {
|
||||
testDescription := fmt.Sprintf("%s #%d", c.input, i)
|
||||
testDescription := fmt.Sprintf("%s #%d (%v)", c.input, i, res.refPairs)
|
||||
assert.Equal(t, e.image, res.refPairs[i].image, testDescription)
|
||||
assert.Equal(t, e.srcRef, transports.ImageName(res.refPairs[i].srcRef), testDescription)
|
||||
assert.Equal(t, e.dstName, storageReferenceWithoutLocation(res.refPairs[i].dstRef), testDescription)
|
||||
}
|
||||
assert.False(t, res.pullAllPairs, c.input)
|
||||
assert.Equal(t, c.expectedUsedSearchRegistries, res.usedSearchRegistries, c.input)
|
||||
if !c.expectedUsedSearchRegistries {
|
||||
assert.Nil(t, res.searchedRegistries, c.input)
|
||||
} else {
|
||||
assert.Equal(t, []string{"example.com", "docker.io"}, res.searchedRegistries, c.input)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1 @@
|
|||
.vscode/*
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
language: go
|
||||
go:
|
||||
- 1.x
|
||||
script:
|
||||
- GOOS=windows go install github.com/chzyer/readline/example/...
|
||||
- GOOS=linux go install github.com/chzyer/readline/example/...
|
||||
- GOOS=darwin go install github.com/chzyer/readline/example/...
|
||||
- go test -race -v
|
||||
|
|
@ -0,0 +1,58 @@
|
|||
# ChangeLog
|
||||
|
||||
### 1.4 - 2016-07-25
|
||||
|
||||
* [#60][60] Support dynamic autocompletion
|
||||
* Fix ANSI parser on Windows
|
||||
* Fix wrong column width in complete mode on Windows
|
||||
* Remove dependent package "golang.org/x/crypto/ssh/terminal"
|
||||
|
||||
### 1.3 - 2016-05-09
|
||||
|
||||
* [#38][38] add SetChildren for prefix completer interface
|
||||
* [#42][42] improve multiple lines compatibility
|
||||
* [#43][43] remove sub-package(runes) for gopkg compatibility
|
||||
* [#46][46] Auto complete with space prefixed line
|
||||
* [#48][48] support suspend process (ctrl+Z)
|
||||
* [#49][49] fix bug that check equals with previous command
|
||||
* [#53][53] Fix bug which causes integer divide by zero panicking when input buffer is empty
|
||||
|
||||
### 1.2 - 2016-03-05
|
||||
|
||||
* Add a demo for checking password strength [example/readline-pass-strength](https://github.com/chzyer/readline/blob/master/example/readline-pass-strength/readline-pass-strength.go), , written by [@sahib](https://github.com/sahib)
|
||||
* [#23][23], support stdin remapping
|
||||
* [#27][27], add a `UniqueEditLine` to `Config`, which will erase the editing line after user submited it, usually use in IM.
|
||||
* Add a demo for multiline [example/readline-multiline](https://github.com/chzyer/readline/blob/master/example/readline-multiline/readline-multiline.go) which can submit one SQL by multiple lines.
|
||||
* Supports performs even stdin/stdout is not a tty.
|
||||
* Add a new simple apis for single instance, check by [here](https://github.com/chzyer/readline/blob/master/std.go). It need to save history manually if using this api.
|
||||
* [#28][28], fixes the history is not working as expected.
|
||||
* [#33][33], vim mode now support `c`, `d`, `x (delete character)`, `r (replace character)`
|
||||
|
||||
### 1.1 - 2015-11-20
|
||||
|
||||
* [#12][12] Add support for key `<Delete>`/`<Home>`/`<End>`
|
||||
* Only enter raw mode as needed (calling `Readline()`), program will receive signal(e.g. Ctrl+C) if not interact with `readline`.
|
||||
* Bugs fixed for `PrefixCompleter`
|
||||
* Press `Ctrl+D` in empty line will cause `io.EOF` in error, Press `Ctrl+C` in anytime will cause `ErrInterrupt` instead of `io.EOF`, this will privodes a shell-like user experience.
|
||||
* Customable Interrupt/EOF prompt in `Config`
|
||||
* [#17][17] Change atomic package to use 32bit function to let it runnable on arm 32bit devices
|
||||
* Provides a new password user experience(`readline.ReadPasswordEx()`).
|
||||
|
||||
### 1.0 - 2015-10-14
|
||||
|
||||
* Initial public release.
|
||||
|
||||
[12]: https://github.com/chzyer/readline/pull/12
|
||||
[17]: https://github.com/chzyer/readline/pull/17
|
||||
[23]: https://github.com/chzyer/readline/pull/23
|
||||
[27]: https://github.com/chzyer/readline/pull/27
|
||||
[28]: https://github.com/chzyer/readline/pull/28
|
||||
[33]: https://github.com/chzyer/readline/pull/33
|
||||
[38]: https://github.com/chzyer/readline/pull/38
|
||||
[42]: https://github.com/chzyer/readline/pull/42
|
||||
[43]: https://github.com/chzyer/readline/pull/43
|
||||
[46]: https://github.com/chzyer/readline/pull/46
|
||||
[48]: https://github.com/chzyer/readline/pull/48
|
||||
[49]: https://github.com/chzyer/readline/pull/49
|
||||
[53]: https://github.com/chzyer/readline/pull/53
|
||||
[60]: https://github.com/chzyer/readline/pull/60
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2015 Chzyer
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
|
|
@ -0,0 +1,114 @@
|
|||
[](https://travis-ci.org/chzyer/readline)
|
||||
[](LICENSE.md)
|
||||
[](https://github.com/chzyer/readline/releases)
|
||||
[](https://godoc.org/github.com/chzyer/readline)
|
||||
[](#backers)
|
||||
[](#sponsors)
|
||||
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/chzyer/readline/assets/logo.png" />
|
||||
<a href="https://asciinema.org/a/32oseof9mkilg7t7d4780qt4m" target="_blank"><img src="https://asciinema.org/a/32oseof9mkilg7t7d4780qt4m.png" width="654"/></a>
|
||||
<img src="https://raw.githubusercontent.com/chzyer/readline/assets/logo_f.png" />
|
||||
</p>
|
||||
|
||||
A powerful readline library in `Linux` `macOS` `Windows` `Solaris`
|
||||
|
||||
## Guide
|
||||
|
||||
* [Demo](example/readline-demo/readline-demo.go)
|
||||
* [Shortcut](doc/shortcut.md)
|
||||
|
||||
## Repos using readline
|
||||
|
||||
[](https://github.com/cockroachdb/cockroach)
|
||||
[](https://github.com/robertkrimen/otto)
|
||||
[](https://github.com/remind101/empire)
|
||||
[](https://github.com/mehrdadrad/mylg)
|
||||
[](https://github.com/knq/usql)
|
||||
[](https://github.com/youtube/doorman)
|
||||
[](https://github.com/bom-d-van/harp)
|
||||
[](https://github.com/abiosoft/ishell)
|
||||
[](https://github.com/Netflix/hal-9001)
|
||||
[](https://github.com/docker/go-p9p)
|
||||
|
||||
|
||||
## Feedback
|
||||
|
||||
If you have any questions, please submit a github issue and any pull requests is welcomed :)
|
||||
|
||||
* [https://twitter.com/chzyer](https://twitter.com/chzyer)
|
||||
* [http://weibo.com/2145262190](http://weibo.com/2145262190)
|
||||
|
||||
|
||||
## Backers
|
||||
|
||||
Love Readline? Help me keep it alive by donating funds to cover project expenses!<br />
|
||||
[[Become a backer](https://opencollective.com/readline#backer)]
|
||||
|
||||
<a href="https://opencollective.com/readline/backer/0/website" target="_blank"><img src="https://opencollective.com/readline/backer/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/1/website" target="_blank"><img src="https://opencollective.com/readline/backer/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/2/website" target="_blank"><img src="https://opencollective.com/readline/backer/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/3/website" target="_blank"><img src="https://opencollective.com/readline/backer/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/4/website" target="_blank"><img src="https://opencollective.com/readline/backer/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/5/website" target="_blank"><img src="https://opencollective.com/readline/backer/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/6/website" target="_blank"><img src="https://opencollective.com/readline/backer/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/7/website" target="_blank"><img src="https://opencollective.com/readline/backer/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/8/website" target="_blank"><img src="https://opencollective.com/readline/backer/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/9/website" target="_blank"><img src="https://opencollective.com/readline/backer/9/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/10/website" target="_blank"><img src="https://opencollective.com/readline/backer/10/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/11/website" target="_blank"><img src="https://opencollective.com/readline/backer/11/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/12/website" target="_blank"><img src="https://opencollective.com/readline/backer/12/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/13/website" target="_blank"><img src="https://opencollective.com/readline/backer/13/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/14/website" target="_blank"><img src="https://opencollective.com/readline/backer/14/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/15/website" target="_blank"><img src="https://opencollective.com/readline/backer/15/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/16/website" target="_blank"><img src="https://opencollective.com/readline/backer/16/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/17/website" target="_blank"><img src="https://opencollective.com/readline/backer/17/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/18/website" target="_blank"><img src="https://opencollective.com/readline/backer/18/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/19/website" target="_blank"><img src="https://opencollective.com/readline/backer/19/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/20/website" target="_blank"><img src="https://opencollective.com/readline/backer/20/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/21/website" target="_blank"><img src="https://opencollective.com/readline/backer/21/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/22/website" target="_blank"><img src="https://opencollective.com/readline/backer/22/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/23/website" target="_blank"><img src="https://opencollective.com/readline/backer/23/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/24/website" target="_blank"><img src="https://opencollective.com/readline/backer/24/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/25/website" target="_blank"><img src="https://opencollective.com/readline/backer/25/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/26/website" target="_blank"><img src="https://opencollective.com/readline/backer/26/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/27/website" target="_blank"><img src="https://opencollective.com/readline/backer/27/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/28/website" target="_blank"><img src="https://opencollective.com/readline/backer/28/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/backer/29/website" target="_blank"><img src="https://opencollective.com/readline/backer/29/avatar.svg"></a>
|
||||
|
||||
|
||||
## Sponsors
|
||||
|
||||
Become a sponsor and get your logo here on our Github page. [[Become a sponsor](https://opencollective.com/readline#sponsor)]
|
||||
|
||||
<a href="https://opencollective.com/readline/sponsor/0/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/0/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/1/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/1/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/2/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/2/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/3/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/3/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/4/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/4/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/5/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/5/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/6/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/6/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/7/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/7/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/8/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/8/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/9/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/9/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/10/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/10/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/11/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/11/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/12/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/12/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/13/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/13/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/14/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/14/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/15/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/15/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/16/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/16/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/17/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/17/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/18/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/18/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/19/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/19/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/20/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/20/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/21/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/21/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/22/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/22/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/23/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/23/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/24/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/24/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/25/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/25/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/26/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/26/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/27/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/27/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/28/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/28/avatar.svg"></a>
|
||||
<a href="https://opencollective.com/readline/sponsor/29/website" target="_blank"><img src="https://opencollective.com/readline/sponsor/29/avatar.svg"></a>
|
||||
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
// +build windows
|
||||
|
||||
package readline
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"unicode/utf8"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
_ = uint16(0)
|
||||
COLOR_FBLUE = 0x0001
|
||||
COLOR_FGREEN = 0x0002
|
||||
COLOR_FRED = 0x0004
|
||||
COLOR_FINTENSITY = 0x0008
|
||||
|
||||
COLOR_BBLUE = 0x0010
|
||||
COLOR_BGREEN = 0x0020
|
||||
COLOR_BRED = 0x0040
|
||||
COLOR_BINTENSITY = 0x0080
|
||||
|
||||
COMMON_LVB_UNDERSCORE = 0x8000
|
||||
COMMON_LVB_BOLD = 0x0007
|
||||
)
|
||||
|
||||
var ColorTableFg = []word{
|
||||
0, // 30: Black
|
||||
COLOR_FRED, // 31: Red
|
||||
COLOR_FGREEN, // 32: Green
|
||||
COLOR_FRED | COLOR_FGREEN, // 33: Yellow
|
||||
COLOR_FBLUE, // 34: Blue
|
||||
COLOR_FRED | COLOR_FBLUE, // 35: Magenta
|
||||
COLOR_FGREEN | COLOR_FBLUE, // 36: Cyan
|
||||
COLOR_FRED | COLOR_FBLUE | COLOR_FGREEN, // 37: White
|
||||
}
|
||||
|
||||
var ColorTableBg = []word{
|
||||
0, // 40: Black
|
||||
COLOR_BRED, // 41: Red
|
||||
COLOR_BGREEN, // 42: Green
|
||||
COLOR_BRED | COLOR_BGREEN, // 43: Yellow
|
||||
COLOR_BBLUE, // 44: Blue
|
||||
COLOR_BRED | COLOR_BBLUE, // 45: Magenta
|
||||
COLOR_BGREEN | COLOR_BBLUE, // 46: Cyan
|
||||
COLOR_BRED | COLOR_BBLUE | COLOR_BGREEN, // 47: White
|
||||
}
|
||||
|
||||
type ANSIWriter struct {
|
||||
target io.Writer
|
||||
wg sync.WaitGroup
|
||||
ctx *ANSIWriterCtx
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
func NewANSIWriter(w io.Writer) *ANSIWriter {
|
||||
a := &ANSIWriter{
|
||||
target: w,
|
||||
ctx: NewANSIWriterCtx(w),
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *ANSIWriter) Close() error {
|
||||
a.wg.Wait()
|
||||
return nil
|
||||
}
|
||||
|
||||
type ANSIWriterCtx struct {
|
||||
isEsc bool
|
||||
isEscSeq bool
|
||||
arg []string
|
||||
target *bufio.Writer
|
||||
wantFlush bool
|
||||
}
|
||||
|
||||
func NewANSIWriterCtx(target io.Writer) *ANSIWriterCtx {
|
||||
return &ANSIWriterCtx{
|
||||
target: bufio.NewWriter(target),
|
||||
}
|
||||
}
|
||||
|
||||
func (a *ANSIWriterCtx) Flush() {
|
||||
a.target.Flush()
|
||||
}
|
||||
|
||||
func (a *ANSIWriterCtx) process(r rune) bool {
|
||||
if a.wantFlush {
|
||||
if r == 0 || r == CharEsc {
|
||||
a.wantFlush = false
|
||||
a.target.Flush()
|
||||
}
|
||||
}
|
||||
if a.isEscSeq {
|
||||
a.isEscSeq = a.ioloopEscSeq(a.target, r, &a.arg)
|
||||
return true
|
||||
}
|
||||
|
||||
switch r {
|
||||
case CharEsc:
|
||||
a.isEsc = true
|
||||
case '[':
|
||||
if a.isEsc {
|
||||
a.arg = nil
|
||||
a.isEscSeq = true
|
||||
a.isEsc = false
|
||||
break
|
||||
}
|
||||
fallthrough
|
||||
default:
|
||||
a.target.WriteRune(r)
|
||||
a.wantFlush = true
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (a *ANSIWriterCtx) ioloopEscSeq(w *bufio.Writer, r rune, argptr *[]string) bool {
|
||||
arg := *argptr
|
||||
var err error
|
||||
|
||||
if r >= 'A' && r <= 'D' {
|
||||
count := short(GetInt(arg, 1))
|
||||
info, err := GetConsoleScreenBufferInfo()
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
switch r {
|
||||
case 'A': // up
|
||||
info.dwCursorPosition.y -= count
|
||||
case 'B': // down
|
||||
info.dwCursorPosition.y += count
|
||||
case 'C': // right
|
||||
info.dwCursorPosition.x += count
|
||||
case 'D': // left
|
||||
info.dwCursorPosition.x -= count
|
||||
}
|
||||
SetConsoleCursorPosition(&info.dwCursorPosition)
|
||||
return false
|
||||
}
|
||||
|
||||
switch r {
|
||||
case 'J':
|
||||
killLines()
|
||||
case 'K':
|
||||
eraseLine()
|
||||
case 'm':
|
||||
color := word(0)
|
||||
for _, item := range arg {
|
||||
var c int
|
||||
c, err = strconv.Atoi(item)
|
||||
if err != nil {
|
||||
w.WriteString("[" + strings.Join(arg, ";") + "m")
|
||||
break
|
||||
}
|
||||
if c >= 30 && c < 40 {
|
||||
color ^= COLOR_FINTENSITY
|
||||
color |= ColorTableFg[c-30]
|
||||
} else if c >= 40 && c < 50 {
|
||||
color ^= COLOR_BINTENSITY
|
||||
color |= ColorTableBg[c-40]
|
||||
} else if c == 4 {
|
||||
color |= COMMON_LVB_UNDERSCORE | ColorTableFg[7]
|
||||
} else if c == 1 {
|
||||
color |= COMMON_LVB_BOLD | COLOR_FINTENSITY
|
||||
} else { // unknown code treat as reset
|
||||
color = ColorTableFg[7]
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
kernel.SetConsoleTextAttribute(stdout, uintptr(color))
|
||||
case '\007': // set title
|
||||
case ';':
|
||||
if len(arg) == 0 || arg[len(arg)-1] != "" {
|
||||
arg = append(arg, "")
|
||||
*argptr = arg
|
||||
}
|
||||
return true
|
||||
default:
|
||||
if len(arg) == 0 {
|
||||
arg = append(arg, "")
|
||||
}
|
||||
arg[len(arg)-1] += string(r)
|
||||
*argptr = arg
|
||||
return true
|
||||
}
|
||||
*argptr = nil
|
||||
return false
|
||||
}
|
||||
|
||||
func (a *ANSIWriter) Write(b []byte) (int, error) {
|
||||
a.Lock()
|
||||
defer a.Unlock()
|
||||
|
||||
off := 0
|
||||
for len(b) > off {
|
||||
r, size := utf8.DecodeRune(b[off:])
|
||||
if size == 0 {
|
||||
return off, io.ErrShortWrite
|
||||
}
|
||||
off += size
|
||||
a.ctx.process(r)
|
||||
}
|
||||
a.ctx.Flush()
|
||||
return off, nil
|
||||
}
|
||||
|
||||
func killLines() error {
|
||||
sbi, err := GetConsoleScreenBufferInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
size := (sbi.dwCursorPosition.y - sbi.dwSize.y) * sbi.dwSize.x
|
||||
size += sbi.dwCursorPosition.x
|
||||
|
||||
var written int
|
||||
kernel.FillConsoleOutputAttribute(stdout, uintptr(ColorTableFg[7]),
|
||||
uintptr(size),
|
||||
sbi.dwCursorPosition.ptr(),
|
||||
uintptr(unsafe.Pointer(&written)),
|
||||
)
|
||||
return kernel.FillConsoleOutputCharacterW(stdout, uintptr(' '),
|
||||
uintptr(size),
|
||||
sbi.dwCursorPosition.ptr(),
|
||||
uintptr(unsafe.Pointer(&written)),
|
||||
)
|
||||
}
|
||||
|
||||
func eraseLine() error {
|
||||
sbi, err := GetConsoleScreenBufferInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
size := sbi.dwSize.x
|
||||
sbi.dwCursorPosition.x = 0
|
||||
var written int
|
||||
return kernel.FillConsoleOutputCharacterW(stdout, uintptr(' '),
|
||||
uintptr(size),
|
||||
sbi.dwCursorPosition.ptr(),
|
||||
uintptr(unsafe.Pointer(&written)),
|
||||
)
|
||||
}
|
||||
|
|
@ -0,0 +1,285 @@
|
|||
package readline
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
type AutoCompleter interface {
|
||||
// Readline will pass the whole line and current offset to it
|
||||
// Completer need to pass all the candidates, and how long they shared the same characters in line
|
||||
// Example:
|
||||
// [go, git, git-shell, grep]
|
||||
// Do("g", 1) => ["o", "it", "it-shell", "rep"], 1
|
||||
// Do("gi", 2) => ["t", "t-shell"], 2
|
||||
// Do("git", 3) => ["", "-shell"], 3
|
||||
Do(line []rune, pos int) (newLine [][]rune, length int)
|
||||
}
|
||||
|
||||
type TabCompleter struct{}
|
||||
|
||||
func (t *TabCompleter) Do([]rune, int) ([][]rune, int) {
|
||||
return [][]rune{[]rune("\t")}, 0
|
||||
}
|
||||
|
||||
type opCompleter struct {
|
||||
w io.Writer
|
||||
op *Operation
|
||||
width int
|
||||
|
||||
inCompleteMode bool
|
||||
inSelectMode bool
|
||||
candidate [][]rune
|
||||
candidateSource []rune
|
||||
candidateOff int
|
||||
candidateChoise int
|
||||
candidateColNum int
|
||||
}
|
||||
|
||||
func newOpCompleter(w io.Writer, op *Operation, width int) *opCompleter {
|
||||
return &opCompleter{
|
||||
w: w,
|
||||
op: op,
|
||||
width: width,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *opCompleter) doSelect() {
|
||||
if len(o.candidate) == 1 {
|
||||
o.op.buf.WriteRunes(o.candidate[0])
|
||||
o.ExitCompleteMode(false)
|
||||
return
|
||||
}
|
||||
o.nextCandidate(1)
|
||||
o.CompleteRefresh()
|
||||
}
|
||||
|
||||
func (o *opCompleter) nextCandidate(i int) {
|
||||
o.candidateChoise += i
|
||||
o.candidateChoise = o.candidateChoise % len(o.candidate)
|
||||
if o.candidateChoise < 0 {
|
||||
o.candidateChoise = len(o.candidate) + o.candidateChoise
|
||||
}
|
||||
}
|
||||
|
||||
func (o *opCompleter) OnComplete() bool {
|
||||
if o.width == 0 {
|
||||
return false
|
||||
}
|
||||
if o.IsInCompleteSelectMode() {
|
||||
o.doSelect()
|
||||
return true
|
||||
}
|
||||
|
||||
buf := o.op.buf
|
||||
rs := buf.Runes()
|
||||
|
||||
if o.IsInCompleteMode() && o.candidateSource != nil && runes.Equal(rs, o.candidateSource) {
|
||||
o.EnterCompleteSelectMode()
|
||||
o.doSelect()
|
||||
return true
|
||||
}
|
||||
|
||||
o.ExitCompleteSelectMode()
|
||||
o.candidateSource = rs
|
||||
newLines, offset := o.op.cfg.AutoComplete.Do(rs, buf.idx)
|
||||
if len(newLines) == 0 {
|
||||
o.ExitCompleteMode(false)
|
||||
return true
|
||||
}
|
||||
|
||||
// only Aggregate candidates in non-complete mode
|
||||
if !o.IsInCompleteMode() {
|
||||
if len(newLines) == 1 {
|
||||
buf.WriteRunes(newLines[0])
|
||||
o.ExitCompleteMode(false)
|
||||
return true
|
||||
}
|
||||
|
||||
same, size := runes.Aggregate(newLines)
|
||||
if size > 0 {
|
||||
buf.WriteRunes(same)
|
||||
o.ExitCompleteMode(false)
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
o.EnterCompleteMode(offset, newLines)
|
||||
return true
|
||||
}
|
||||
|
||||
func (o *opCompleter) IsInCompleteSelectMode() bool {
|
||||
return o.inSelectMode
|
||||
}
|
||||
|
||||
func (o *opCompleter) IsInCompleteMode() bool {
|
||||
return o.inCompleteMode
|
||||
}
|
||||
|
||||
func (o *opCompleter) HandleCompleteSelect(r rune) bool {
|
||||
next := true
|
||||
switch r {
|
||||
case CharEnter, CharCtrlJ:
|
||||
next = false
|
||||
o.op.buf.WriteRunes(o.op.candidate[o.op.candidateChoise])
|
||||
o.ExitCompleteMode(false)
|
||||
case CharLineStart:
|
||||
num := o.candidateChoise % o.candidateColNum
|
||||
o.nextCandidate(-num)
|
||||
case CharLineEnd:
|
||||
num := o.candidateColNum - o.candidateChoise%o.candidateColNum - 1
|
||||
o.candidateChoise += num
|
||||
if o.candidateChoise >= len(o.candidate) {
|
||||
o.candidateChoise = len(o.candidate) - 1
|
||||
}
|
||||
case CharBackspace:
|
||||
o.ExitCompleteSelectMode()
|
||||
next = false
|
||||
case CharTab, CharForward:
|
||||
o.doSelect()
|
||||
case CharBell, CharInterrupt:
|
||||
o.ExitCompleteMode(true)
|
||||
next = false
|
||||
case CharNext:
|
||||
tmpChoise := o.candidateChoise + o.candidateColNum
|
||||
if tmpChoise >= o.getMatrixSize() {
|
||||
tmpChoise -= o.getMatrixSize()
|
||||
} else if tmpChoise >= len(o.candidate) {
|
||||
tmpChoise += o.candidateColNum
|
||||
tmpChoise -= o.getMatrixSize()
|
||||
}
|
||||
o.candidateChoise = tmpChoise
|
||||
case CharBackward:
|
||||
o.nextCandidate(-1)
|
||||
case CharPrev:
|
||||
tmpChoise := o.candidateChoise - o.candidateColNum
|
||||
if tmpChoise < 0 {
|
||||
tmpChoise += o.getMatrixSize()
|
||||
if tmpChoise >= len(o.candidate) {
|
||||
tmpChoise -= o.candidateColNum
|
||||
}
|
||||
}
|
||||
o.candidateChoise = tmpChoise
|
||||
default:
|
||||
next = false
|
||||
o.ExitCompleteSelectMode()
|
||||
}
|
||||
if next {
|
||||
o.CompleteRefresh()
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (o *opCompleter) getMatrixSize() int {
|
||||
line := len(o.candidate) / o.candidateColNum
|
||||
if len(o.candidate)%o.candidateColNum != 0 {
|
||||
line++
|
||||
}
|
||||
return line * o.candidateColNum
|
||||
}
|
||||
|
||||
func (o *opCompleter) OnWidthChange(newWidth int) {
|
||||
o.width = newWidth
|
||||
}
|
||||
|
||||
func (o *opCompleter) CompleteRefresh() {
|
||||
if !o.inCompleteMode {
|
||||
return
|
||||
}
|
||||
lineCnt := o.op.buf.CursorLineCount()
|
||||
colWidth := 0
|
||||
for _, c := range o.candidate {
|
||||
w := runes.WidthAll(c)
|
||||
if w > colWidth {
|
||||
colWidth = w
|
||||
}
|
||||
}
|
||||
colWidth += o.candidateOff + 1
|
||||
same := o.op.buf.RuneSlice(-o.candidateOff)
|
||||
|
||||
// -1 to avoid reach the end of line
|
||||
width := o.width - 1
|
||||
colNum := width / colWidth
|
||||
if colNum != 0 {
|
||||
colWidth += (width - (colWidth * colNum)) / colNum
|
||||
}
|
||||
|
||||
o.candidateColNum = colNum
|
||||
buf := bufio.NewWriter(o.w)
|
||||
buf.Write(bytes.Repeat([]byte("\n"), lineCnt))
|
||||
|
||||
colIdx := 0
|
||||
lines := 1
|
||||
buf.WriteString("\033[J")
|
||||
for idx, c := range o.candidate {
|
||||
inSelect := idx == o.candidateChoise && o.IsInCompleteSelectMode()
|
||||
if inSelect {
|
||||
buf.WriteString("\033[30;47m")
|
||||
}
|
||||
buf.WriteString(string(same))
|
||||
buf.WriteString(string(c))
|
||||
buf.Write(bytes.Repeat([]byte(" "), colWidth-runes.WidthAll(c)-runes.WidthAll(same)))
|
||||
|
||||
if inSelect {
|
||||
buf.WriteString("\033[0m")
|
||||
}
|
||||
|
||||
colIdx++
|
||||
if colIdx == colNum {
|
||||
buf.WriteString("\n")
|
||||
lines++
|
||||
colIdx = 0
|
||||
}
|
||||
}
|
||||
|
||||
// move back
|
||||
fmt.Fprintf(buf, "\033[%dA\r", lineCnt-1+lines)
|
||||
fmt.Fprintf(buf, "\033[%dC", o.op.buf.idx+o.op.buf.PromptLen())
|
||||
buf.Flush()
|
||||
}
|
||||
|
||||
func (o *opCompleter) aggCandidate(candidate [][]rune) int {
|
||||
offset := 0
|
||||
for i := 0; i < len(candidate[0]); i++ {
|
||||
for j := 0; j < len(candidate)-1; j++ {
|
||||
if i > len(candidate[j]) {
|
||||
goto aggregate
|
||||
}
|
||||
if candidate[j][i] != candidate[j+1][i] {
|
||||
goto aggregate
|
||||
}
|
||||
}
|
||||
offset = i
|
||||
}
|
||||
aggregate:
|
||||
return offset
|
||||
}
|
||||
|
||||
func (o *opCompleter) EnterCompleteSelectMode() {
|
||||
o.inSelectMode = true
|
||||
o.candidateChoise = -1
|
||||
o.CompleteRefresh()
|
||||
}
|
||||
|
||||
func (o *opCompleter) EnterCompleteMode(offset int, candidate [][]rune) {
|
||||
o.inCompleteMode = true
|
||||
o.candidate = candidate
|
||||
o.candidateOff = offset
|
||||
o.CompleteRefresh()
|
||||
}
|
||||
|
||||
func (o *opCompleter) ExitCompleteSelectMode() {
|
||||
o.inSelectMode = false
|
||||
o.candidate = nil
|
||||
o.candidateChoise = -1
|
||||
o.candidateOff = -1
|
||||
o.candidateSource = nil
|
||||
}
|
||||
|
||||
func (o *opCompleter) ExitCompleteMode(revent bool) {
|
||||
o.inCompleteMode = false
|
||||
o.ExitCompleteSelectMode()
|
||||
}
|
||||
|
|
@ -0,0 +1,165 @@
|
|||
package readline
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Caller type for dynamic completion
|
||||
type DynamicCompleteFunc func(string) []string
|
||||
|
||||
type PrefixCompleterInterface interface {
|
||||
Print(prefix string, level int, buf *bytes.Buffer)
|
||||
Do(line []rune, pos int) (newLine [][]rune, length int)
|
||||
GetName() []rune
|
||||
GetChildren() []PrefixCompleterInterface
|
||||
SetChildren(children []PrefixCompleterInterface)
|
||||
}
|
||||
|
||||
type DynamicPrefixCompleterInterface interface {
|
||||
PrefixCompleterInterface
|
||||
IsDynamic() bool
|
||||
GetDynamicNames(line []rune) [][]rune
|
||||
}
|
||||
|
||||
type PrefixCompleter struct {
|
||||
Name []rune
|
||||
Dynamic bool
|
||||
Callback DynamicCompleteFunc
|
||||
Children []PrefixCompleterInterface
|
||||
}
|
||||
|
||||
func (p *PrefixCompleter) Tree(prefix string) string {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
p.Print(prefix, 0, buf)
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func Print(p PrefixCompleterInterface, prefix string, level int, buf *bytes.Buffer) {
|
||||
if strings.TrimSpace(string(p.GetName())) != "" {
|
||||
buf.WriteString(prefix)
|
||||
if level > 0 {
|
||||
buf.WriteString("├")
|
||||
buf.WriteString(strings.Repeat("─", (level*4)-2))
|
||||
buf.WriteString(" ")
|
||||
}
|
||||
buf.WriteString(string(p.GetName()) + "\n")
|
||||
level++
|
||||
}
|
||||
for _, ch := range p.GetChildren() {
|
||||
ch.Print(prefix, level, buf)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PrefixCompleter) Print(prefix string, level int, buf *bytes.Buffer) {
|
||||
Print(p, prefix, level, buf)
|
||||
}
|
||||
|
||||
func (p *PrefixCompleter) IsDynamic() bool {
|
||||
return p.Dynamic
|
||||
}
|
||||
|
||||
func (p *PrefixCompleter) GetName() []rune {
|
||||
return p.Name
|
||||
}
|
||||
|
||||
func (p *PrefixCompleter) GetDynamicNames(line []rune) [][]rune {
|
||||
var names = [][]rune{}
|
||||
for _, name := range p.Callback(string(line)) {
|
||||
names = append(names, []rune(name+" "))
|
||||
}
|
||||
return names
|
||||
}
|
||||
|
||||
func (p *PrefixCompleter) GetChildren() []PrefixCompleterInterface {
|
||||
return p.Children
|
||||
}
|
||||
|
||||
func (p *PrefixCompleter) SetChildren(children []PrefixCompleterInterface) {
|
||||
p.Children = children
|
||||
}
|
||||
|
||||
func NewPrefixCompleter(pc ...PrefixCompleterInterface) *PrefixCompleter {
|
||||
return PcItem("", pc...)
|
||||
}
|
||||
|
||||
func PcItem(name string, pc ...PrefixCompleterInterface) *PrefixCompleter {
|
||||
name += " "
|
||||
return &PrefixCompleter{
|
||||
Name: []rune(name),
|
||||
Dynamic: false,
|
||||
Children: pc,
|
||||
}
|
||||
}
|
||||
|
||||
func PcItemDynamic(callback DynamicCompleteFunc, pc ...PrefixCompleterInterface) *PrefixCompleter {
|
||||
return &PrefixCompleter{
|
||||
Callback: callback,
|
||||
Dynamic: true,
|
||||
Children: pc,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *PrefixCompleter) Do(line []rune, pos int) (newLine [][]rune, offset int) {
|
||||
return doInternal(p, line, pos, line)
|
||||
}
|
||||
|
||||
func Do(p PrefixCompleterInterface, line []rune, pos int) (newLine [][]rune, offset int) {
|
||||
return doInternal(p, line, pos, line)
|
||||
}
|
||||
|
||||
func doInternal(p PrefixCompleterInterface, line []rune, pos int, origLine []rune) (newLine [][]rune, offset int) {
|
||||
line = runes.TrimSpaceLeft(line[:pos])
|
||||
goNext := false
|
||||
var lineCompleter PrefixCompleterInterface
|
||||
for _, child := range p.GetChildren() {
|
||||
childNames := make([][]rune, 1)
|
||||
|
||||
childDynamic, ok := child.(DynamicPrefixCompleterInterface)
|
||||
if ok && childDynamic.IsDynamic() {
|
||||
childNames = childDynamic.GetDynamicNames(origLine)
|
||||
} else {
|
||||
childNames[0] = child.GetName()
|
||||
}
|
||||
|
||||
for _, childName := range childNames {
|
||||
if len(line) >= len(childName) {
|
||||
if runes.HasPrefix(line, childName) {
|
||||
if len(line) == len(childName) {
|
||||
newLine = append(newLine, []rune{' '})
|
||||
} else {
|
||||
newLine = append(newLine, childName)
|
||||
}
|
||||
offset = len(childName)
|
||||
lineCompleter = child
|
||||
goNext = true
|
||||
}
|
||||
} else {
|
||||
if runes.HasPrefix(childName, line) {
|
||||
newLine = append(newLine, childName[len(line):])
|
||||
offset = len(line)
|
||||
lineCompleter = child
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(newLine) != 1 {
|
||||
return
|
||||
}
|
||||
|
||||
tmpLine := make([]rune, 0, len(line))
|
||||
for i := offset; i < len(line); i++ {
|
||||
if line[i] == ' ' {
|
||||
continue
|
||||
}
|
||||
|
||||
tmpLine = append(tmpLine, line[i:]...)
|
||||
return doInternal(lineCompleter, tmpLine, len(tmpLine), origLine)
|
||||
}
|
||||
|
||||
if goNext {
|
||||
return doInternal(lineCompleter, nil, 0, origLine)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
package readline
|
||||
|
||||
type SegmentCompleter interface {
|
||||
// a
|
||||
// |- a1
|
||||
// |--- a11
|
||||
// |- a2
|
||||
// b
|
||||
// input:
|
||||
// DoTree([], 0) [a, b]
|
||||
// DoTree([a], 1) [a]
|
||||
// DoTree([a, ], 0) [a1, a2]
|
||||
// DoTree([a, a], 1) [a1, a2]
|
||||
// DoTree([a, a1], 2) [a1]
|
||||
// DoTree([a, a1, ], 0) [a11]
|
||||
// DoTree([a, a1, a], 1) [a11]
|
||||
DoSegment([][]rune, int) [][]rune
|
||||
}
|
||||
|
||||
type dumpSegmentCompleter struct {
|
||||
f func([][]rune, int) [][]rune
|
||||
}
|
||||
|
||||
func (d *dumpSegmentCompleter) DoSegment(segment [][]rune, n int) [][]rune {
|
||||
return d.f(segment, n)
|
||||
}
|
||||
|
||||
func SegmentFunc(f func([][]rune, int) [][]rune) AutoCompleter {
|
||||
return &SegmentComplete{&dumpSegmentCompleter{f}}
|
||||
}
|
||||
|
||||
func SegmentAutoComplete(completer SegmentCompleter) *SegmentComplete {
|
||||
return &SegmentComplete{
|
||||
SegmentCompleter: completer,
|
||||
}
|
||||
}
|
||||
|
||||
type SegmentComplete struct {
|
||||
SegmentCompleter
|
||||
}
|
||||
|
||||
func RetSegment(segments [][]rune, cands [][]rune, idx int) ([][]rune, int) {
|
||||
ret := make([][]rune, 0, len(cands))
|
||||
lastSegment := segments[len(segments)-1]
|
||||
for _, cand := range cands {
|
||||
if !runes.HasPrefix(cand, lastSegment) {
|
||||
continue
|
||||
}
|
||||
ret = append(ret, cand[len(lastSegment):])
|
||||
}
|
||||
return ret, idx
|
||||
}
|
||||
|
||||
func SplitSegment(line []rune, pos int) ([][]rune, int) {
|
||||
segs := [][]rune{}
|
||||
lastIdx := -1
|
||||
line = line[:pos]
|
||||
pos = 0
|
||||
for idx, l := range line {
|
||||
if l == ' ' {
|
||||
pos = 0
|
||||
segs = append(segs, line[lastIdx+1:idx])
|
||||
lastIdx = idx
|
||||
} else {
|
||||
pos++
|
||||
}
|
||||
}
|
||||
segs = append(segs, line[lastIdx+1:])
|
||||
return segs, pos
|
||||
}
|
||||
|
||||
func (c *SegmentComplete) Do(line []rune, pos int) (newLine [][]rune, offset int) {
|
||||
|
||||
segment, idx := SplitSegment(line, pos)
|
||||
|
||||
cands := c.DoSegment(segment, idx)
|
||||
newLine, offset = RetSegment(segment, cands, idx)
|
||||
for idx := range newLine {
|
||||
newLine[idx] = append(newLine[idx], ' ')
|
||||
}
|
||||
return newLine, offset
|
||||
}
|
||||
|
|
@ -0,0 +1,330 @@
|
|||
package readline
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"container/list"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type hisItem struct {
|
||||
Source []rune
|
||||
Version int64
|
||||
Tmp []rune
|
||||
}
|
||||
|
||||
func (h *hisItem) Clean() {
|
||||
h.Source = nil
|
||||
h.Tmp = nil
|
||||
}
|
||||
|
||||
type opHistory struct {
|
||||
cfg *Config
|
||||
history *list.List
|
||||
historyVer int64
|
||||
current *list.Element
|
||||
fd *os.File
|
||||
fdLock sync.Mutex
|
||||
enable bool
|
||||
}
|
||||
|
||||
func newOpHistory(cfg *Config) (o *opHistory) {
|
||||
o = &opHistory{
|
||||
cfg: cfg,
|
||||
history: list.New(),
|
||||
enable: true,
|
||||
}
|
||||
return o
|
||||
}
|
||||
|
||||
func (o *opHistory) Reset() {
|
||||
o.history = list.New()
|
||||
o.current = nil
|
||||
}
|
||||
|
||||
func (o *opHistory) IsHistoryClosed() bool {
|
||||
o.fdLock.Lock()
|
||||
defer o.fdLock.Unlock()
|
||||
return o.fd.Fd() == ^(uintptr(0))
|
||||
}
|
||||
|
||||
func (o *opHistory) Init() {
|
||||
if o.IsHistoryClosed() {
|
||||
o.initHistory()
|
||||
}
|
||||
}
|
||||
|
||||
func (o *opHistory) initHistory() {
|
||||
if o.cfg.HistoryFile != "" {
|
||||
o.historyUpdatePath(o.cfg.HistoryFile)
|
||||
}
|
||||
}
|
||||
|
||||
// only called by newOpHistory
|
||||
func (o *opHistory) historyUpdatePath(path string) {
|
||||
o.fdLock.Lock()
|
||||
defer o.fdLock.Unlock()
|
||||
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_RDWR, 0666)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
o.fd = f
|
||||
r := bufio.NewReader(o.fd)
|
||||
total := 0
|
||||
for ; ; total++ {
|
||||
line, err := r.ReadString('\n')
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
// ignore the empty line
|
||||
line = strings.TrimSpace(line)
|
||||
if len(line) == 0 {
|
||||
continue
|
||||
}
|
||||
o.Push([]rune(line))
|
||||
o.Compact()
|
||||
}
|
||||
if total > o.cfg.HistoryLimit {
|
||||
o.rewriteLocked()
|
||||
}
|
||||
o.historyVer++
|
||||
o.Push(nil)
|
||||
return
|
||||
}
|
||||
|
||||
func (o *opHistory) Compact() {
|
||||
for o.history.Len() > o.cfg.HistoryLimit && o.history.Len() > 0 {
|
||||
o.history.Remove(o.history.Front())
|
||||
}
|
||||
}
|
||||
|
||||
func (o *opHistory) Rewrite() {
|
||||
o.fdLock.Lock()
|
||||
defer o.fdLock.Unlock()
|
||||
o.rewriteLocked()
|
||||
}
|
||||
|
||||
func (o *opHistory) rewriteLocked() {
|
||||
if o.cfg.HistoryFile == "" {
|
||||
return
|
||||
}
|
||||
|
||||
tmpFile := o.cfg.HistoryFile + ".tmp"
|
||||
fd, err := os.OpenFile(tmpFile, os.O_CREATE|os.O_WRONLY|os.O_TRUNC|os.O_APPEND, 0666)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
buf := bufio.NewWriter(fd)
|
||||
for elem := o.history.Front(); elem != nil; elem = elem.Next() {
|
||||
buf.WriteString(string(elem.Value.(*hisItem).Source) + "\n")
|
||||
}
|
||||
buf.Flush()
|
||||
|
||||
// replace history file
|
||||
if err = os.Rename(tmpFile, o.cfg.HistoryFile); err != nil {
|
||||
fd.Close()
|
||||
return
|
||||
}
|
||||
|
||||
if o.fd != nil {
|
||||
o.fd.Close()
|
||||
}
|
||||
// fd is write only, just satisfy what we need.
|
||||
o.fd = fd
|
||||
}
|
||||
|
||||
func (o *opHistory) Close() {
|
||||
o.fdLock.Lock()
|
||||
defer o.fdLock.Unlock()
|
||||
if o.fd != nil {
|
||||
o.fd.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (o *opHistory) FindBck(isNewSearch bool, rs []rune, start int) (int, *list.Element) {
|
||||
for elem := o.current; elem != nil; elem = elem.Prev() {
|
||||
item := o.showItem(elem.Value)
|
||||
if isNewSearch {
|
||||
start += len(rs)
|
||||
}
|
||||
if elem == o.current {
|
||||
if len(item) >= start {
|
||||
item = item[:start]
|
||||
}
|
||||
}
|
||||
idx := runes.IndexAllBckEx(item, rs, o.cfg.HistorySearchFold)
|
||||
if idx < 0 {
|
||||
continue
|
||||
}
|
||||
return idx, elem
|
||||
}
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
func (o *opHistory) FindFwd(isNewSearch bool, rs []rune, start int) (int, *list.Element) {
|
||||
for elem := o.current; elem != nil; elem = elem.Next() {
|
||||
item := o.showItem(elem.Value)
|
||||
if isNewSearch {
|
||||
start -= len(rs)
|
||||
if start < 0 {
|
||||
start = 0
|
||||
}
|
||||
}
|
||||
if elem == o.current {
|
||||
if len(item)-1 >= start {
|
||||
item = item[start:]
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
}
|
||||
idx := runes.IndexAllEx(item, rs, o.cfg.HistorySearchFold)
|
||||
if idx < 0 {
|
||||
continue
|
||||
}
|
||||
if elem == o.current {
|
||||
idx += start
|
||||
}
|
||||
return idx, elem
|
||||
}
|
||||
return -1, nil
|
||||
}
|
||||
|
||||
func (o *opHistory) showItem(obj interface{}) []rune {
|
||||
item := obj.(*hisItem)
|
||||
if item.Version == o.historyVer {
|
||||
return item.Tmp
|
||||
}
|
||||
return item.Source
|
||||
}
|
||||
|
||||
func (o *opHistory) Prev() []rune {
|
||||
if o.current == nil {
|
||||
return nil
|
||||
}
|
||||
current := o.current.Prev()
|
||||
if current == nil {
|
||||
return nil
|
||||
}
|
||||
o.current = current
|
||||
return runes.Copy(o.showItem(current.Value))
|
||||
}
|
||||
|
||||
func (o *opHistory) Next() ([]rune, bool) {
|
||||
if o.current == nil {
|
||||
return nil, false
|
||||
}
|
||||
current := o.current.Next()
|
||||
if current == nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
o.current = current
|
||||
return runes.Copy(o.showItem(current.Value)), true
|
||||
}
|
||||
|
||||
// Disable the current history
|
||||
func (o *opHistory) Disable() {
|
||||
o.enable = false
|
||||
}
|
||||
|
||||
// Enable the current history
|
||||
func (o *opHistory) Enable() {
|
||||
o.enable = true
|
||||
}
|
||||
|
||||
func (o *opHistory) debug() {
|
||||
Debug("-------")
|
||||
for item := o.history.Front(); item != nil; item = item.Next() {
|
||||
Debug(fmt.Sprintf("%+v", item.Value))
|
||||
}
|
||||
}
|
||||
|
||||
// save history
|
||||
func (o *opHistory) New(current []rune) (err error) {
|
||||
|
||||
// history deactivated
|
||||
if !o.enable {
|
||||
return nil
|
||||
}
|
||||
|
||||
current = runes.Copy(current)
|
||||
|
||||
// if just use last command without modify
|
||||
// just clean lastest history
|
||||
if back := o.history.Back(); back != nil {
|
||||
prev := back.Prev()
|
||||
if prev != nil {
|
||||
if runes.Equal(current, prev.Value.(*hisItem).Source) {
|
||||
o.current = o.history.Back()
|
||||
o.current.Value.(*hisItem).Clean()
|
||||
o.historyVer++
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(current) == 0 {
|
||||
o.current = o.history.Back()
|
||||
if o.current != nil {
|
||||
o.current.Value.(*hisItem).Clean()
|
||||
o.historyVer++
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if o.current != o.history.Back() {
|
||||
// move history item to current command
|
||||
currentItem := o.current.Value.(*hisItem)
|
||||
// set current to last item
|
||||
o.current = o.history.Back()
|
||||
|
||||
current = runes.Copy(currentItem.Tmp)
|
||||
}
|
||||
|
||||
// err only can be a IO error, just report
|
||||
err = o.Update(current, true)
|
||||
|
||||
// push a new one to commit current command
|
||||
o.historyVer++
|
||||
o.Push(nil)
|
||||
return
|
||||
}
|
||||
|
||||
func (o *opHistory) Revert() {
|
||||
o.historyVer++
|
||||
o.current = o.history.Back()
|
||||
}
|
||||
|
||||
func (o *opHistory) Update(s []rune, commit bool) (err error) {
|
||||
o.fdLock.Lock()
|
||||
defer o.fdLock.Unlock()
|
||||
s = runes.Copy(s)
|
||||
if o.current == nil {
|
||||
o.Push(s)
|
||||
o.Compact()
|
||||
return
|
||||
}
|
||||
r := o.current.Value.(*hisItem)
|
||||
r.Version = o.historyVer
|
||||
if commit {
|
||||
r.Source = s
|
||||
if o.fd != nil {
|
||||
// just report the error
|
||||
_, err = o.fd.Write([]byte(string(r.Source) + "\n"))
|
||||
}
|
||||
} else {
|
||||
r.Tmp = append(r.Tmp[:0], s...)
|
||||
}
|
||||
o.current.Value = r
|
||||
o.Compact()
|
||||
return
|
||||
}
|
||||
|
||||
func (o *opHistory) Push(s []rune) {
|
||||
s = runes.Copy(s)
|
||||
elem := o.history.PushBack(&hisItem{Source: s})
|
||||
o.current = elem
|
||||
}
|
||||
|
|
@ -0,0 +1,531 @@
|
|||
package readline
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"sync"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrInterrupt = errors.New("Interrupt")
|
||||
)
|
||||
|
||||
type InterruptError struct {
|
||||
Line []rune
|
||||
}
|
||||
|
||||
func (*InterruptError) Error() string {
|
||||
return "Interrupted"
|
||||
}
|
||||
|
||||
type Operation struct {
|
||||
m sync.Mutex
|
||||
cfg *Config
|
||||
t *Terminal
|
||||
buf *RuneBuffer
|
||||
outchan chan []rune
|
||||
errchan chan error
|
||||
w io.Writer
|
||||
|
||||
history *opHistory
|
||||
*opSearch
|
||||
*opCompleter
|
||||
*opPassword
|
||||
*opVim
|
||||
}
|
||||
|
||||
func (o *Operation) SetBuffer(what string) {
|
||||
o.buf.Set([]rune(what))
|
||||
}
|
||||
|
||||
type wrapWriter struct {
|
||||
r *Operation
|
||||
t *Terminal
|
||||
target io.Writer
|
||||
}
|
||||
|
||||
func (w *wrapWriter) Write(b []byte) (int, error) {
|
||||
if !w.t.IsReading() {
|
||||
return w.target.Write(b)
|
||||
}
|
||||
|
||||
var (
|
||||
n int
|
||||
err error
|
||||
)
|
||||
w.r.buf.Refresh(func() {
|
||||
n, err = w.target.Write(b)
|
||||
})
|
||||
|
||||
if w.r.IsSearchMode() {
|
||||
w.r.SearchRefresh(-1)
|
||||
}
|
||||
if w.r.IsInCompleteMode() {
|
||||
w.r.CompleteRefresh()
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
func NewOperation(t *Terminal, cfg *Config) *Operation {
|
||||
width := cfg.FuncGetWidth()
|
||||
op := &Operation{
|
||||
t: t,
|
||||
buf: NewRuneBuffer(t, cfg.Prompt, cfg, width),
|
||||
outchan: make(chan []rune),
|
||||
errchan: make(chan error, 1),
|
||||
}
|
||||
op.w = op.buf.w
|
||||
op.SetConfig(cfg)
|
||||
op.opVim = newVimMode(op)
|
||||
op.opCompleter = newOpCompleter(op.buf.w, op, width)
|
||||
op.opPassword = newOpPassword(op)
|
||||
op.cfg.FuncOnWidthChanged(func() {
|
||||
newWidth := cfg.FuncGetWidth()
|
||||
op.opCompleter.OnWidthChange(newWidth)
|
||||
op.opSearch.OnWidthChange(newWidth)
|
||||
op.buf.OnWidthChange(newWidth)
|
||||
})
|
||||
go op.ioloop()
|
||||
return op
|
||||
}
|
||||
|
||||
func (o *Operation) SetPrompt(s string) {
|
||||
o.buf.SetPrompt(s)
|
||||
}
|
||||
|
||||
func (o *Operation) SetMaskRune(r rune) {
|
||||
o.buf.SetMask(r)
|
||||
}
|
||||
|
||||
func (o *Operation) GetConfig() *Config {
|
||||
o.m.Lock()
|
||||
cfg := *o.cfg
|
||||
o.m.Unlock()
|
||||
return &cfg
|
||||
}
|
||||
|
||||
func (o *Operation) ioloop() {
|
||||
for {
|
||||
keepInSearchMode := false
|
||||
keepInCompleteMode := false
|
||||
r := o.t.ReadRune()
|
||||
if o.GetConfig().FuncFilterInputRune != nil {
|
||||
var process bool
|
||||
r, process = o.GetConfig().FuncFilterInputRune(r)
|
||||
if !process {
|
||||
o.buf.Refresh(nil) // to refresh the line
|
||||
continue // ignore this rune
|
||||
}
|
||||
}
|
||||
|
||||
if r == 0 { // io.EOF
|
||||
if o.buf.Len() == 0 {
|
||||
o.buf.Clean()
|
||||
select {
|
||||
case o.errchan <- io.EOF:
|
||||
}
|
||||
break
|
||||
} else {
|
||||
// if stdin got io.EOF and there is something left in buffer,
|
||||
// let's flush them by sending CharEnter.
|
||||
// And we will got io.EOF int next loop.
|
||||
r = CharEnter
|
||||
}
|
||||
}
|
||||
isUpdateHistory := true
|
||||
|
||||
if o.IsInCompleteSelectMode() {
|
||||
keepInCompleteMode = o.HandleCompleteSelect(r)
|
||||
if keepInCompleteMode {
|
||||
continue
|
||||
}
|
||||
|
||||
o.buf.Refresh(nil)
|
||||
switch r {
|
||||
case CharEnter, CharCtrlJ:
|
||||
o.history.Update(o.buf.Runes(), false)
|
||||
fallthrough
|
||||
case CharInterrupt:
|
||||
o.t.KickRead()
|
||||
fallthrough
|
||||
case CharBell:
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if o.IsEnableVimMode() {
|
||||
r = o.HandleVim(r, o.t.ReadRune)
|
||||
if r == 0 {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
switch r {
|
||||
case CharBell:
|
||||
if o.IsSearchMode() {
|
||||
o.ExitSearchMode(true)
|
||||
o.buf.Refresh(nil)
|
||||
}
|
||||
if o.IsInCompleteMode() {
|
||||
o.ExitCompleteMode(true)
|
||||
o.buf.Refresh(nil)
|
||||
}
|
||||
case CharTab:
|
||||
if o.GetConfig().AutoComplete == nil {
|
||||
o.t.Bell()
|
||||
break
|
||||
}
|
||||
if o.OnComplete() {
|
||||
keepInCompleteMode = true
|
||||
} else {
|
||||
o.t.Bell()
|
||||
break
|
||||
}
|
||||
|
||||
case CharBckSearch:
|
||||
if !o.SearchMode(S_DIR_BCK) {
|
||||
o.t.Bell()
|
||||
break
|
||||
}
|
||||
keepInSearchMode = true
|
||||
case CharCtrlU:
|
||||
o.buf.KillFront()
|
||||
case CharFwdSearch:
|
||||
if !o.SearchMode(S_DIR_FWD) {
|
||||
o.t.Bell()
|
||||
break
|
||||
}
|
||||
keepInSearchMode = true
|
||||
case CharKill:
|
||||
o.buf.Kill()
|
||||
keepInCompleteMode = true
|
||||
case MetaForward:
|
||||
o.buf.MoveToNextWord()
|
||||
case CharTranspose:
|
||||
o.buf.Transpose()
|
||||
case MetaBackward:
|
||||
o.buf.MoveToPrevWord()
|
||||
case MetaDelete:
|
||||
o.buf.DeleteWord()
|
||||
case CharLineStart:
|
||||
o.buf.MoveToLineStart()
|
||||
case CharLineEnd:
|
||||
o.buf.MoveToLineEnd()
|
||||
case CharBackspace, CharCtrlH:
|
||||
if o.IsSearchMode() {
|
||||
o.SearchBackspace()
|
||||
keepInSearchMode = true
|
||||
break
|
||||
}
|
||||
|
||||
if o.buf.Len() == 0 {
|
||||
o.t.Bell()
|
||||
break
|
||||
}
|
||||
o.buf.Backspace()
|
||||
if o.IsInCompleteMode() {
|
||||
o.OnComplete()
|
||||
}
|
||||
case CharCtrlZ:
|
||||
o.buf.Clean()
|
||||
o.t.SleepToResume()
|
||||
o.Refresh()
|
||||
case CharCtrlL:
|
||||
ClearScreen(o.w)
|
||||
o.Refresh()
|
||||
case MetaBackspace, CharCtrlW:
|
||||
o.buf.BackEscapeWord()
|
||||
case CharCtrlY:
|
||||
o.buf.Yank()
|
||||
case CharEnter, CharCtrlJ:
|
||||
if o.IsSearchMode() {
|
||||
o.ExitSearchMode(false)
|
||||
}
|
||||
o.buf.MoveToLineEnd()
|
||||
var data []rune
|
||||
if !o.GetConfig().UniqueEditLine {
|
||||
o.buf.WriteRune('\n')
|
||||
data = o.buf.Reset()
|
||||
data = data[:len(data)-1] // trim \n
|
||||
} else {
|
||||
o.buf.Clean()
|
||||
data = o.buf.Reset()
|
||||
}
|
||||
o.outchan <- data
|
||||
if !o.GetConfig().DisableAutoSaveHistory {
|
||||
// ignore IO error
|
||||
_ = o.history.New(data)
|
||||
} else {
|
||||
isUpdateHistory = false
|
||||
}
|
||||
case CharBackward:
|
||||
o.buf.MoveBackward()
|
||||
case CharForward:
|
||||
o.buf.MoveForward()
|
||||
case CharPrev:
|
||||
buf := o.history.Prev()
|
||||
if buf != nil {
|
||||
o.buf.Set(buf)
|
||||
} else {
|
||||
o.t.Bell()
|
||||
}
|
||||
case CharNext:
|
||||
buf, ok := o.history.Next()
|
||||
if ok {
|
||||
o.buf.Set(buf)
|
||||
} else {
|
||||
o.t.Bell()
|
||||
}
|
||||
case CharDelete:
|
||||
if o.buf.Len() > 0 || !o.IsNormalMode() {
|
||||
o.t.KickRead()
|
||||
if !o.buf.Delete() {
|
||||
o.t.Bell()
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// treat as EOF
|
||||
if !o.GetConfig().UniqueEditLine {
|
||||
o.buf.WriteString(o.GetConfig().EOFPrompt + "\n")
|
||||
}
|
||||
o.buf.Reset()
|
||||
isUpdateHistory = false
|
||||
o.history.Revert()
|
||||
o.errchan <- io.EOF
|
||||
if o.GetConfig().UniqueEditLine {
|
||||
o.buf.Clean()
|
||||
}
|
||||
case CharInterrupt:
|
||||
if o.IsSearchMode() {
|
||||
o.t.KickRead()
|
||||
o.ExitSearchMode(true)
|
||||
break
|
||||
}
|
||||
if o.IsInCompleteMode() {
|
||||
o.t.KickRead()
|
||||
o.ExitCompleteMode(true)
|
||||
o.buf.Refresh(nil)
|
||||
break
|
||||
}
|
||||
o.buf.MoveToLineEnd()
|
||||
o.buf.Refresh(nil)
|
||||
hint := o.GetConfig().InterruptPrompt + "\n"
|
||||
if !o.GetConfig().UniqueEditLine {
|
||||
o.buf.WriteString(hint)
|
||||
}
|
||||
remain := o.buf.Reset()
|
||||
if !o.GetConfig().UniqueEditLine {
|
||||
remain = remain[:len(remain)-len([]rune(hint))]
|
||||
}
|
||||
isUpdateHistory = false
|
||||
o.history.Revert()
|
||||
o.errchan <- &InterruptError{remain}
|
||||
default:
|
||||
if o.IsSearchMode() {
|
||||
o.SearchChar(r)
|
||||
keepInSearchMode = true
|
||||
break
|
||||
}
|
||||
o.buf.WriteRune(r)
|
||||
if o.IsInCompleteMode() {
|
||||
o.OnComplete()
|
||||
keepInCompleteMode = true
|
||||
}
|
||||
}
|
||||
|
||||
listener := o.GetConfig().Listener
|
||||
if listener != nil {
|
||||
newLine, newPos, ok := listener.OnChange(o.buf.Runes(), o.buf.Pos(), r)
|
||||
if ok {
|
||||
o.buf.SetWithIdx(newPos, newLine)
|
||||
}
|
||||
}
|
||||
|
||||
o.m.Lock()
|
||||
if !keepInSearchMode && o.IsSearchMode() {
|
||||
o.ExitSearchMode(false)
|
||||
o.buf.Refresh(nil)
|
||||
} else if o.IsInCompleteMode() {
|
||||
if !keepInCompleteMode {
|
||||
o.ExitCompleteMode(false)
|
||||
o.Refresh()
|
||||
} else {
|
||||
o.buf.Refresh(nil)
|
||||
o.CompleteRefresh()
|
||||
}
|
||||
}
|
||||
if isUpdateHistory && !o.IsSearchMode() {
|
||||
// it will cause null history
|
||||
o.history.Update(o.buf.Runes(), false)
|
||||
}
|
||||
o.m.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Operation) Stderr() io.Writer {
|
||||
return &wrapWriter{target: o.GetConfig().Stderr, r: o, t: o.t}
|
||||
}
|
||||
|
||||
func (o *Operation) Stdout() io.Writer {
|
||||
return &wrapWriter{target: o.GetConfig().Stdout, r: o, t: o.t}
|
||||
}
|
||||
|
||||
func (o *Operation) String() (string, error) {
|
||||
r, err := o.Runes()
|
||||
return string(r), err
|
||||
}
|
||||
|
||||
func (o *Operation) Runes() ([]rune, error) {
|
||||
o.t.EnterRawMode()
|
||||
defer o.t.ExitRawMode()
|
||||
|
||||
listener := o.GetConfig().Listener
|
||||
if listener != nil {
|
||||
listener.OnChange(nil, 0, 0)
|
||||
}
|
||||
|
||||
o.buf.Refresh(nil) // print prompt
|
||||
o.t.KickRead()
|
||||
select {
|
||||
case r := <-o.outchan:
|
||||
return r, nil
|
||||
case err := <-o.errchan:
|
||||
if e, ok := err.(*InterruptError); ok {
|
||||
return e.Line, ErrInterrupt
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Operation) PasswordEx(prompt string, l Listener) ([]byte, error) {
|
||||
cfg := o.GenPasswordConfig()
|
||||
cfg.Prompt = prompt
|
||||
cfg.Listener = l
|
||||
return o.PasswordWithConfig(cfg)
|
||||
}
|
||||
|
||||
func (o *Operation) GenPasswordConfig() *Config {
|
||||
return o.opPassword.PasswordConfig()
|
||||
}
|
||||
|
||||
func (o *Operation) PasswordWithConfig(cfg *Config) ([]byte, error) {
|
||||
if err := o.opPassword.EnterPasswordMode(cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer o.opPassword.ExitPasswordMode()
|
||||
return o.Slice()
|
||||
}
|
||||
|
||||
func (o *Operation) Password(prompt string) ([]byte, error) {
|
||||
return o.PasswordEx(prompt, nil)
|
||||
}
|
||||
|
||||
func (o *Operation) SetTitle(t string) {
|
||||
o.w.Write([]byte("\033[2;" + t + "\007"))
|
||||
}
|
||||
|
||||
func (o *Operation) Slice() ([]byte, error) {
|
||||
r, err := o.Runes()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []byte(string(r)), nil
|
||||
}
|
||||
|
||||
func (o *Operation) Close() {
|
||||
o.history.Close()
|
||||
}
|
||||
|
||||
func (o *Operation) SetHistoryPath(path string) {
|
||||
if o.history != nil {
|
||||
o.history.Close()
|
||||
}
|
||||
o.cfg.HistoryFile = path
|
||||
o.history = newOpHistory(o.cfg)
|
||||
}
|
||||
|
||||
func (o *Operation) IsNormalMode() bool {
|
||||
return !o.IsInCompleteMode() && !o.IsSearchMode()
|
||||
}
|
||||
|
||||
func (op *Operation) SetConfig(cfg *Config) (*Config, error) {
|
||||
op.m.Lock()
|
||||
defer op.m.Unlock()
|
||||
if op.cfg == cfg {
|
||||
return op.cfg, nil
|
||||
}
|
||||
if err := cfg.Init(); err != nil {
|
||||
return op.cfg, err
|
||||
}
|
||||
old := op.cfg
|
||||
op.cfg = cfg
|
||||
op.SetPrompt(cfg.Prompt)
|
||||
op.SetMaskRune(cfg.MaskRune)
|
||||
op.buf.SetConfig(cfg)
|
||||
width := op.cfg.FuncGetWidth()
|
||||
|
||||
if cfg.opHistory == nil {
|
||||
op.SetHistoryPath(cfg.HistoryFile)
|
||||
cfg.opHistory = op.history
|
||||
cfg.opSearch = newOpSearch(op.buf.w, op.buf, op.history, cfg, width)
|
||||
}
|
||||
op.history = cfg.opHistory
|
||||
|
||||
// SetHistoryPath will close opHistory which already exists
|
||||
// so if we use it next time, we need to reopen it by `InitHistory()`
|
||||
op.history.Init()
|
||||
|
||||
if op.cfg.AutoComplete != nil {
|
||||
op.opCompleter = newOpCompleter(op.buf.w, op, width)
|
||||
}
|
||||
|
||||
op.opSearch = cfg.opSearch
|
||||
return old, nil
|
||||
}
|
||||
|
||||
func (o *Operation) ResetHistory() {
|
||||
o.history.Reset()
|
||||
}
|
||||
|
||||
// if err is not nil, it just mean it fail to write to file
|
||||
// other things goes fine.
|
||||
func (o *Operation) SaveHistory(content string) error {
|
||||
return o.history.New([]rune(content))
|
||||
}
|
||||
|
||||
func (o *Operation) Refresh() {
|
||||
if o.t.IsReading() {
|
||||
o.buf.Refresh(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *Operation) Clean() {
|
||||
o.buf.Clean()
|
||||
}
|
||||
|
||||
func FuncListener(f func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool)) Listener {
|
||||
return &DumpListener{f: f}
|
||||
}
|
||||
|
||||
type DumpListener struct {
|
||||
f func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool)
|
||||
}
|
||||
|
||||
func (d *DumpListener) OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool) {
|
||||
return d.f(line, pos, key)
|
||||
}
|
||||
|
||||
type Listener interface {
|
||||
OnChange(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool)
|
||||
}
|
||||
|
||||
type Painter interface {
|
||||
Paint(line []rune, pos int) []rune
|
||||
}
|
||||
|
||||
type defaultPainter struct{}
|
||||
|
||||
func (p *defaultPainter) Paint(line []rune, _ int) []rune {
|
||||
return line
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package readline
|
||||
|
||||
type opPassword struct {
|
||||
o *Operation
|
||||
backupCfg *Config
|
||||
}
|
||||
|
||||
func newOpPassword(o *Operation) *opPassword {
|
||||
return &opPassword{o: o}
|
||||
}
|
||||
|
||||
func (o *opPassword) ExitPasswordMode() {
|
||||
o.o.SetConfig(o.backupCfg)
|
||||
o.backupCfg = nil
|
||||
}
|
||||
|
||||
func (o *opPassword) EnterPasswordMode(cfg *Config) (err error) {
|
||||
o.backupCfg, err = o.o.SetConfig(cfg)
|
||||
return
|
||||
}
|
||||
|
||||
func (o *opPassword) PasswordConfig() *Config {
|
||||
return &Config{
|
||||
EnableMask: true,
|
||||
InterruptPrompt: "\n",
|
||||
EOFPrompt: "\n",
|
||||
HistoryLimit: -1,
|
||||
Painter: &defaultPainter{},
|
||||
|
||||
Stdout: o.o.cfg.Stdout,
|
||||
Stderr: o.o.cfg.Stderr,
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,125 @@
|
|||
// +build windows
|
||||
|
||||
package readline
|
||||
|
||||
import "unsafe"
|
||||
|
||||
const (
|
||||
VK_CANCEL = 0x03
|
||||
VK_BACK = 0x08
|
||||
VK_TAB = 0x09
|
||||
VK_RETURN = 0x0D
|
||||
VK_SHIFT = 0x10
|
||||
VK_CONTROL = 0x11
|
||||
VK_MENU = 0x12
|
||||
VK_ESCAPE = 0x1B
|
||||
VK_LEFT = 0x25
|
||||
VK_UP = 0x26
|
||||
VK_RIGHT = 0x27
|
||||
VK_DOWN = 0x28
|
||||
VK_DELETE = 0x2E
|
||||
VK_LSHIFT = 0xA0
|
||||
VK_RSHIFT = 0xA1
|
||||
VK_LCONTROL = 0xA2
|
||||
VK_RCONTROL = 0xA3
|
||||
)
|
||||
|
||||
// RawReader translate input record to ANSI escape sequence.
|
||||
// To provides same behavior as unix terminal.
|
||||
type RawReader struct {
|
||||
ctrlKey bool
|
||||
altKey bool
|
||||
}
|
||||
|
||||
func NewRawReader() *RawReader {
|
||||
r := new(RawReader)
|
||||
return r
|
||||
}
|
||||
|
||||
// only process one action in one read
|
||||
func (r *RawReader) Read(buf []byte) (int, error) {
|
||||
ir := new(_INPUT_RECORD)
|
||||
var read int
|
||||
var err error
|
||||
next:
|
||||
err = kernel.ReadConsoleInputW(stdin,
|
||||
uintptr(unsafe.Pointer(ir)),
|
||||
1,
|
||||
uintptr(unsafe.Pointer(&read)),
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if ir.EventType != EVENT_KEY {
|
||||
goto next
|
||||
}
|
||||
ker := (*_KEY_EVENT_RECORD)(unsafe.Pointer(&ir.Event[0]))
|
||||
if ker.bKeyDown == 0 { // keyup
|
||||
if r.ctrlKey || r.altKey {
|
||||
switch ker.wVirtualKeyCode {
|
||||
case VK_RCONTROL, VK_LCONTROL:
|
||||
r.ctrlKey = false
|
||||
case VK_MENU: //alt
|
||||
r.altKey = false
|
||||
}
|
||||
}
|
||||
goto next
|
||||
}
|
||||
|
||||
if ker.unicodeChar == 0 {
|
||||
var target rune
|
||||
switch ker.wVirtualKeyCode {
|
||||
case VK_RCONTROL, VK_LCONTROL:
|
||||
r.ctrlKey = true
|
||||
case VK_MENU: //alt
|
||||
r.altKey = true
|
||||
case VK_LEFT:
|
||||
target = CharBackward
|
||||
case VK_RIGHT:
|
||||
target = CharForward
|
||||
case VK_UP:
|
||||
target = CharPrev
|
||||
case VK_DOWN:
|
||||
target = CharNext
|
||||
}
|
||||
if target != 0 {
|
||||
return r.write(buf, target)
|
||||
}
|
||||
goto next
|
||||
}
|
||||
char := rune(ker.unicodeChar)
|
||||
if r.ctrlKey {
|
||||
switch char {
|
||||
case 'A':
|
||||
char = CharLineStart
|
||||
case 'E':
|
||||
char = CharLineEnd
|
||||
case 'R':
|
||||
char = CharBckSearch
|
||||
case 'S':
|
||||
char = CharFwdSearch
|
||||
}
|
||||
} else if r.altKey {
|
||||
switch char {
|
||||
case VK_BACK:
|
||||
char = CharBackspace
|
||||
}
|
||||
return r.writeEsc(buf, char)
|
||||
}
|
||||
return r.write(buf, char)
|
||||
}
|
||||
|
||||
func (r *RawReader) writeEsc(b []byte, char rune) (int, error) {
|
||||
b[0] = '\033'
|
||||
n := copy(b[1:], []byte(string(char)))
|
||||
return n + 1, nil
|
||||
}
|
||||
|
||||
func (r *RawReader) write(b []byte, char rune) (int, error) {
|
||||
n := copy(b, []byte(string(char)))
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (r *RawReader) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,326 @@
|
|||
// Readline is a pure go implementation for GNU-Readline kind library.
|
||||
//
|
||||
// example:
|
||||
// rl, err := readline.New("> ")
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
// defer rl.Close()
|
||||
//
|
||||
// for {
|
||||
// line, err := rl.Readline()
|
||||
// if err != nil { // io.EOF
|
||||
// break
|
||||
// }
|
||||
// println(line)
|
||||
// }
|
||||
//
|
||||
package readline
|
||||
|
||||
import "io"
|
||||
|
||||
type Instance struct {
|
||||
Config *Config
|
||||
Terminal *Terminal
|
||||
Operation *Operation
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
// prompt supports ANSI escape sequence, so we can color some characters even in windows
|
||||
Prompt string
|
||||
|
||||
// readline will persist historys to file where HistoryFile specified
|
||||
HistoryFile string
|
||||
// specify the max length of historys, it's 500 by default, set it to -1 to disable history
|
||||
HistoryLimit int
|
||||
DisableAutoSaveHistory bool
|
||||
// enable case-insensitive history searching
|
||||
HistorySearchFold bool
|
||||
|
||||
// AutoCompleter will called once user press TAB
|
||||
AutoComplete AutoCompleter
|
||||
|
||||
// Any key press will pass to Listener
|
||||
// NOTE: Listener will be triggered by (nil, 0, 0) immediately
|
||||
Listener Listener
|
||||
|
||||
Painter Painter
|
||||
|
||||
// If VimMode is true, readline will in vim.insert mode by default
|
||||
VimMode bool
|
||||
|
||||
InterruptPrompt string
|
||||
EOFPrompt string
|
||||
|
||||
FuncGetWidth func() int
|
||||
|
||||
Stdin io.ReadCloser
|
||||
StdinWriter io.Writer
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
|
||||
EnableMask bool
|
||||
MaskRune rune
|
||||
|
||||
// erase the editing line after user submited it
|
||||
// it use in IM usually.
|
||||
UniqueEditLine bool
|
||||
|
||||
// filter input runes (may be used to disable CtrlZ or for translating some keys to different actions)
|
||||
// -> output = new (translated) rune and true/false if continue with processing this one
|
||||
FuncFilterInputRune func(rune) (rune, bool)
|
||||
|
||||
// force use interactive even stdout is not a tty
|
||||
FuncIsTerminal func() bool
|
||||
FuncMakeRaw func() error
|
||||
FuncExitRaw func() error
|
||||
FuncOnWidthChanged func(func())
|
||||
ForceUseInteractive bool
|
||||
|
||||
// private fields
|
||||
inited bool
|
||||
opHistory *opHistory
|
||||
opSearch *opSearch
|
||||
}
|
||||
|
||||
func (c *Config) useInteractive() bool {
|
||||
if c.ForceUseInteractive {
|
||||
return true
|
||||
}
|
||||
return c.FuncIsTerminal()
|
||||
}
|
||||
|
||||
func (c *Config) Init() error {
|
||||
if c.inited {
|
||||
return nil
|
||||
}
|
||||
c.inited = true
|
||||
if c.Stdin == nil {
|
||||
c.Stdin = NewCancelableStdin(Stdin)
|
||||
}
|
||||
|
||||
c.Stdin, c.StdinWriter = NewFillableStdin(c.Stdin)
|
||||
|
||||
if c.Stdout == nil {
|
||||
c.Stdout = Stdout
|
||||
}
|
||||
if c.Stderr == nil {
|
||||
c.Stderr = Stderr
|
||||
}
|
||||
if c.HistoryLimit == 0 {
|
||||
c.HistoryLimit = 500
|
||||
}
|
||||
|
||||
if c.InterruptPrompt == "" {
|
||||
c.InterruptPrompt = "^C"
|
||||
} else if c.InterruptPrompt == "\n" {
|
||||
c.InterruptPrompt = ""
|
||||
}
|
||||
if c.EOFPrompt == "" {
|
||||
c.EOFPrompt = "^D"
|
||||
} else if c.EOFPrompt == "\n" {
|
||||
c.EOFPrompt = ""
|
||||
}
|
||||
|
||||
if c.AutoComplete == nil {
|
||||
c.AutoComplete = &TabCompleter{}
|
||||
}
|
||||
if c.FuncGetWidth == nil {
|
||||
c.FuncGetWidth = GetScreenWidth
|
||||
}
|
||||
if c.FuncIsTerminal == nil {
|
||||
c.FuncIsTerminal = DefaultIsTerminal
|
||||
}
|
||||
rm := new(RawMode)
|
||||
if c.FuncMakeRaw == nil {
|
||||
c.FuncMakeRaw = rm.Enter
|
||||
}
|
||||
if c.FuncExitRaw == nil {
|
||||
c.FuncExitRaw = rm.Exit
|
||||
}
|
||||
if c.FuncOnWidthChanged == nil {
|
||||
c.FuncOnWidthChanged = DefaultOnWidthChanged
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Config) Clone() *Config {
|
||||
c.opHistory = nil
|
||||
c.opSearch = nil
|
||||
return &c
|
||||
}
|
||||
|
||||
func (c *Config) SetListener(f func(line []rune, pos int, key rune) (newLine []rune, newPos int, ok bool)) {
|
||||
c.Listener = FuncListener(f)
|
||||
}
|
||||
|
||||
func (c *Config) SetPainter(p Painter) {
|
||||
c.Painter = p
|
||||
}
|
||||
|
||||
func NewEx(cfg *Config) (*Instance, error) {
|
||||
t, err := NewTerminal(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rl := t.Readline()
|
||||
if cfg.Painter == nil {
|
||||
cfg.Painter = &defaultPainter{}
|
||||
}
|
||||
return &Instance{
|
||||
Config: cfg,
|
||||
Terminal: t,
|
||||
Operation: rl,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func New(prompt string) (*Instance, error) {
|
||||
return NewEx(&Config{Prompt: prompt})
|
||||
}
|
||||
|
||||
func (i *Instance) ResetHistory() {
|
||||
i.Operation.ResetHistory()
|
||||
}
|
||||
|
||||
func (i *Instance) SetPrompt(s string) {
|
||||
i.Operation.SetPrompt(s)
|
||||
}
|
||||
|
||||
func (i *Instance) SetMaskRune(r rune) {
|
||||
i.Operation.SetMaskRune(r)
|
||||
}
|
||||
|
||||
// change history persistence in runtime
|
||||
func (i *Instance) SetHistoryPath(p string) {
|
||||
i.Operation.SetHistoryPath(p)
|
||||
}
|
||||
|
||||
// readline will refresh automatic when write through Stdout()
|
||||
func (i *Instance) Stdout() io.Writer {
|
||||
return i.Operation.Stdout()
|
||||
}
|
||||
|
||||
// readline will refresh automatic when write through Stdout()
|
||||
func (i *Instance) Stderr() io.Writer {
|
||||
return i.Operation.Stderr()
|
||||
}
|
||||
|
||||
// switch VimMode in runtime
|
||||
func (i *Instance) SetVimMode(on bool) {
|
||||
i.Operation.SetVimMode(on)
|
||||
}
|
||||
|
||||
func (i *Instance) IsVimMode() bool {
|
||||
return i.Operation.IsEnableVimMode()
|
||||
}
|
||||
|
||||
func (i *Instance) GenPasswordConfig() *Config {
|
||||
return i.Operation.GenPasswordConfig()
|
||||
}
|
||||
|
||||
// we can generate a config by `i.GenPasswordConfig()`
|
||||
func (i *Instance) ReadPasswordWithConfig(cfg *Config) ([]byte, error) {
|
||||
return i.Operation.PasswordWithConfig(cfg)
|
||||
}
|
||||
|
||||
func (i *Instance) ReadPasswordEx(prompt string, l Listener) ([]byte, error) {
|
||||
return i.Operation.PasswordEx(prompt, l)
|
||||
}
|
||||
|
||||
func (i *Instance) ReadPassword(prompt string) ([]byte, error) {
|
||||
return i.Operation.Password(prompt)
|
||||
}
|
||||
|
||||
type Result struct {
|
||||
Line string
|
||||
Error error
|
||||
}
|
||||
|
||||
func (l *Result) CanContinue() bool {
|
||||
return len(l.Line) != 0 && l.Error == ErrInterrupt
|
||||
}
|
||||
|
||||
func (l *Result) CanBreak() bool {
|
||||
return !l.CanContinue() && l.Error != nil
|
||||
}
|
||||
|
||||
func (i *Instance) Line() *Result {
|
||||
ret, err := i.Readline()
|
||||
return &Result{ret, err}
|
||||
}
|
||||
|
||||
// err is one of (nil, io.EOF, readline.ErrInterrupt)
|
||||
func (i *Instance) Readline() (string, error) {
|
||||
return i.Operation.String()
|
||||
}
|
||||
|
||||
func (i *Instance) ReadlineWithDefault(what string) (string, error) {
|
||||
i.Operation.SetBuffer(what)
|
||||
return i.Operation.String()
|
||||
}
|
||||
|
||||
func (i *Instance) SaveHistory(content string) error {
|
||||
return i.Operation.SaveHistory(content)
|
||||
}
|
||||
|
||||
// same as readline
|
||||
func (i *Instance) ReadSlice() ([]byte, error) {
|
||||
return i.Operation.Slice()
|
||||
}
|
||||
|
||||
// we must make sure that call Close() before process exit.
|
||||
func (i *Instance) Close() error {
|
||||
if err := i.Terminal.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
i.Config.Stdin.Close()
|
||||
i.Operation.Close()
|
||||
return nil
|
||||
}
|
||||
func (i *Instance) Clean() {
|
||||
i.Operation.Clean()
|
||||
}
|
||||
|
||||
func (i *Instance) Write(b []byte) (int, error) {
|
||||
return i.Stdout().Write(b)
|
||||
}
|
||||
|
||||
// WriteStdin prefill the next Stdin fetch
|
||||
// Next time you call ReadLine() this value will be writen before the user input
|
||||
// ie :
|
||||
// i := readline.New()
|
||||
// i.WriteStdin([]byte("test"))
|
||||
// _, _= i.Readline()
|
||||
//
|
||||
// gives
|
||||
//
|
||||
// > test[cursor]
|
||||
func (i *Instance) WriteStdin(val []byte) (int, error) {
|
||||
return i.Terminal.WriteStdin(val)
|
||||
}
|
||||
|
||||
func (i *Instance) SetConfig(cfg *Config) *Config {
|
||||
if i.Config == cfg {
|
||||
return cfg
|
||||
}
|
||||
old := i.Config
|
||||
i.Config = cfg
|
||||
i.Operation.SetConfig(cfg)
|
||||
i.Terminal.SetConfig(cfg)
|
||||
return old
|
||||
}
|
||||
|
||||
func (i *Instance) Refresh() {
|
||||
i.Operation.Refresh()
|
||||
}
|
||||
|
||||
// HistoryDisable the save of the commands into the history
|
||||
func (i *Instance) HistoryDisable() {
|
||||
i.Operation.history.Disable()
|
||||
}
|
||||
|
||||
// HistoryEnable the save of the commands into the history (default on)
|
||||
func (i *Instance) HistoryEnable() {
|
||||
i.Operation.history.Enable()
|
||||
}
|
||||
|
|
@ -0,0 +1,475 @@
|
|||
package readline
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type MsgType int16
|
||||
|
||||
const (
|
||||
T_DATA = MsgType(iota)
|
||||
T_WIDTH
|
||||
T_WIDTH_REPORT
|
||||
T_ISTTY_REPORT
|
||||
T_RAW
|
||||
T_ERAW // exit raw
|
||||
T_EOF
|
||||
)
|
||||
|
||||
type RemoteSvr struct {
|
||||
eof int32
|
||||
closed int32
|
||||
width int32
|
||||
reciveChan chan struct{}
|
||||
writeChan chan *writeCtx
|
||||
conn net.Conn
|
||||
isTerminal bool
|
||||
funcWidthChan func()
|
||||
stopChan chan struct{}
|
||||
|
||||
dataBufM sync.Mutex
|
||||
dataBuf bytes.Buffer
|
||||
}
|
||||
|
||||
type writeReply struct {
|
||||
n int
|
||||
err error
|
||||
}
|
||||
|
||||
type writeCtx struct {
|
||||
msg *Message
|
||||
reply chan *writeReply
|
||||
}
|
||||
|
||||
func newWriteCtx(msg *Message) *writeCtx {
|
||||
return &writeCtx{
|
||||
msg: msg,
|
||||
reply: make(chan *writeReply),
|
||||
}
|
||||
}
|
||||
|
||||
func NewRemoteSvr(conn net.Conn) (*RemoteSvr, error) {
|
||||
rs := &RemoteSvr{
|
||||
width: -1,
|
||||
conn: conn,
|
||||
writeChan: make(chan *writeCtx),
|
||||
reciveChan: make(chan struct{}),
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
buf := bufio.NewReader(rs.conn)
|
||||
|
||||
if err := rs.init(buf); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
go rs.readLoop(buf)
|
||||
go rs.writeLoop()
|
||||
return rs, nil
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) init(buf *bufio.Reader) error {
|
||||
m, err := ReadMessage(buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// receive isTerminal
|
||||
if m.Type != T_ISTTY_REPORT {
|
||||
return fmt.Errorf("unexpected init message")
|
||||
}
|
||||
r.GotIsTerminal(m.Data)
|
||||
|
||||
// receive width
|
||||
m, err = ReadMessage(buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if m.Type != T_WIDTH_REPORT {
|
||||
return fmt.Errorf("unexpected init message")
|
||||
}
|
||||
r.GotReportWidth(m.Data)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) HandleConfig(cfg *Config) {
|
||||
cfg.Stderr = r
|
||||
cfg.Stdout = r
|
||||
cfg.Stdin = r
|
||||
cfg.FuncExitRaw = r.ExitRawMode
|
||||
cfg.FuncIsTerminal = r.IsTerminal
|
||||
cfg.FuncMakeRaw = r.EnterRawMode
|
||||
cfg.FuncExitRaw = r.ExitRawMode
|
||||
cfg.FuncGetWidth = r.GetWidth
|
||||
cfg.FuncOnWidthChanged = func(f func()) {
|
||||
r.funcWidthChan = f
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) IsTerminal() bool {
|
||||
return r.isTerminal
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) checkEOF() error {
|
||||
if atomic.LoadInt32(&r.eof) == 1 {
|
||||
return io.EOF
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) Read(b []byte) (int, error) {
|
||||
r.dataBufM.Lock()
|
||||
n, err := r.dataBuf.Read(b)
|
||||
r.dataBufM.Unlock()
|
||||
if n == 0 {
|
||||
if err := r.checkEOF(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
if n == 0 && err == io.EOF {
|
||||
<-r.reciveChan
|
||||
r.dataBufM.Lock()
|
||||
n, err = r.dataBuf.Read(b)
|
||||
r.dataBufM.Unlock()
|
||||
}
|
||||
if n == 0 {
|
||||
if err := r.checkEOF(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) writeMsg(m *Message) error {
|
||||
ctx := newWriteCtx(m)
|
||||
r.writeChan <- ctx
|
||||
reply := <-ctx.reply
|
||||
return reply.err
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) Write(b []byte) (int, error) {
|
||||
ctx := newWriteCtx(NewMessage(T_DATA, b))
|
||||
r.writeChan <- ctx
|
||||
reply := <-ctx.reply
|
||||
return reply.n, reply.err
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) EnterRawMode() error {
|
||||
return r.writeMsg(NewMessage(T_RAW, nil))
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) ExitRawMode() error {
|
||||
return r.writeMsg(NewMessage(T_ERAW, nil))
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) writeLoop() {
|
||||
defer r.Close()
|
||||
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case ctx, ok := <-r.writeChan:
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
n, err := ctx.msg.WriteTo(r.conn)
|
||||
ctx.reply <- &writeReply{n, err}
|
||||
case <-r.stopChan:
|
||||
break loop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) Close() error {
|
||||
if atomic.CompareAndSwapInt32(&r.closed, 0, 1) {
|
||||
close(r.stopChan)
|
||||
r.conn.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) readLoop(buf *bufio.Reader) {
|
||||
defer r.Close()
|
||||
for {
|
||||
m, err := ReadMessage(buf)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
switch m.Type {
|
||||
case T_EOF:
|
||||
atomic.StoreInt32(&r.eof, 1)
|
||||
select {
|
||||
case r.reciveChan <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
case T_DATA:
|
||||
r.dataBufM.Lock()
|
||||
r.dataBuf.Write(m.Data)
|
||||
r.dataBufM.Unlock()
|
||||
select {
|
||||
case r.reciveChan <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
case T_WIDTH_REPORT:
|
||||
r.GotReportWidth(m.Data)
|
||||
case T_ISTTY_REPORT:
|
||||
r.GotIsTerminal(m.Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) GotIsTerminal(data []byte) {
|
||||
if binary.BigEndian.Uint16(data) == 0 {
|
||||
r.isTerminal = false
|
||||
} else {
|
||||
r.isTerminal = true
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) GotReportWidth(data []byte) {
|
||||
atomic.StoreInt32(&r.width, int32(binary.BigEndian.Uint16(data)))
|
||||
if r.funcWidthChan != nil {
|
||||
r.funcWidthChan()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RemoteSvr) GetWidth() int {
|
||||
return int(atomic.LoadInt32(&r.width))
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
type Message struct {
|
||||
Type MsgType
|
||||
Data []byte
|
||||
}
|
||||
|
||||
func ReadMessage(r io.Reader) (*Message, error) {
|
||||
m := new(Message)
|
||||
var length int32
|
||||
if err := binary.Read(r, binary.BigEndian, &length); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := binary.Read(r, binary.BigEndian, &m.Type); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.Data = make([]byte, int(length)-2)
|
||||
if _, err := io.ReadFull(r, m.Data); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func NewMessage(t MsgType, data []byte) *Message {
|
||||
return &Message{t, data}
|
||||
}
|
||||
|
||||
func (m *Message) WriteTo(w io.Writer) (int, error) {
|
||||
buf := bytes.NewBuffer(make([]byte, 0, len(m.Data)+2+4))
|
||||
binary.Write(buf, binary.BigEndian, int32(len(m.Data)+2))
|
||||
binary.Write(buf, binary.BigEndian, m.Type)
|
||||
buf.Write(m.Data)
|
||||
n, err := buf.WriteTo(w)
|
||||
return int(n), err
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
type RemoteCli struct {
|
||||
conn net.Conn
|
||||
raw RawMode
|
||||
receiveChan chan struct{}
|
||||
inited int32
|
||||
isTerminal *bool
|
||||
|
||||
data bytes.Buffer
|
||||
dataM sync.Mutex
|
||||
}
|
||||
|
||||
func NewRemoteCli(conn net.Conn) (*RemoteCli, error) {
|
||||
r := &RemoteCli{
|
||||
conn: conn,
|
||||
receiveChan: make(chan struct{}),
|
||||
}
|
||||
return r, nil
|
||||
}
|
||||
|
||||
func (r *RemoteCli) MarkIsTerminal(is bool) {
|
||||
r.isTerminal = &is
|
||||
}
|
||||
|
||||
func (r *RemoteCli) init() error {
|
||||
if !atomic.CompareAndSwapInt32(&r.inited, 0, 1) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := r.reportIsTerminal(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := r.reportWidth(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// register sig for width changed
|
||||
DefaultOnWidthChanged(func() {
|
||||
r.reportWidth()
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RemoteCli) writeMsg(m *Message) error {
|
||||
r.dataM.Lock()
|
||||
_, err := m.WriteTo(r.conn)
|
||||
r.dataM.Unlock()
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *RemoteCli) Write(b []byte) (int, error) {
|
||||
m := NewMessage(T_DATA, b)
|
||||
r.dataM.Lock()
|
||||
_, err := m.WriteTo(r.conn)
|
||||
r.dataM.Unlock()
|
||||
return len(b), err
|
||||
}
|
||||
|
||||
func (r *RemoteCli) reportWidth() error {
|
||||
screenWidth := GetScreenWidth()
|
||||
data := make([]byte, 2)
|
||||
binary.BigEndian.PutUint16(data, uint16(screenWidth))
|
||||
msg := NewMessage(T_WIDTH_REPORT, data)
|
||||
|
||||
if err := r.writeMsg(msg); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RemoteCli) reportIsTerminal() error {
|
||||
var isTerminal bool
|
||||
if r.isTerminal != nil {
|
||||
isTerminal = *r.isTerminal
|
||||
} else {
|
||||
isTerminal = DefaultIsTerminal()
|
||||
}
|
||||
data := make([]byte, 2)
|
||||
if isTerminal {
|
||||
binary.BigEndian.PutUint16(data, 1)
|
||||
} else {
|
||||
binary.BigEndian.PutUint16(data, 0)
|
||||
}
|
||||
msg := NewMessage(T_ISTTY_REPORT, data)
|
||||
if err := r.writeMsg(msg); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RemoteCli) readLoop() {
|
||||
buf := bufio.NewReader(r.conn)
|
||||
for {
|
||||
msg, err := ReadMessage(buf)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
switch msg.Type {
|
||||
case T_ERAW:
|
||||
r.raw.Exit()
|
||||
case T_RAW:
|
||||
r.raw.Enter()
|
||||
case T_DATA:
|
||||
os.Stdout.Write(msg.Data)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *RemoteCli) ServeBy(source io.Reader) error {
|
||||
if err := r.init(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer r.Close()
|
||||
for {
|
||||
n, _ := io.Copy(r, source)
|
||||
if n == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}()
|
||||
defer r.raw.Exit()
|
||||
r.readLoop()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RemoteCli) Close() {
|
||||
r.writeMsg(NewMessage(T_EOF, nil))
|
||||
}
|
||||
|
||||
func (r *RemoteCli) Serve() error {
|
||||
return r.ServeBy(os.Stdin)
|
||||
}
|
||||
|
||||
func ListenRemote(n, addr string, cfg *Config, h func(*Instance), onListen ...func(net.Listener) error) error {
|
||||
ln, err := net.Listen(n, addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(onListen) > 0 {
|
||||
if err := onListen[0](ln); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for {
|
||||
conn, err := ln.Accept()
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
go func() {
|
||||
defer conn.Close()
|
||||
rl, err := HandleConn(*cfg, conn)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
h(rl)
|
||||
}()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func HandleConn(cfg Config, conn net.Conn) (*Instance, error) {
|
||||
r, err := NewRemoteSvr(conn)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
r.HandleConfig(&cfg)
|
||||
|
||||
rl, err := NewEx(&cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return rl, nil
|
||||
}
|
||||
|
||||
func DialRemote(n, addr string) error {
|
||||
conn, err := net.Dial(n, addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
cli, err := NewRemoteCli(conn)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return cli.Serve()
|
||||
}
|
||||
|
|
@ -0,0 +1,629 @@
|
|||
package readline
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type runeBufferBck struct {
|
||||
buf []rune
|
||||
idx int
|
||||
}
|
||||
|
||||
type RuneBuffer struct {
|
||||
buf []rune
|
||||
idx int
|
||||
prompt []rune
|
||||
w io.Writer
|
||||
|
||||
hadClean bool
|
||||
interactive bool
|
||||
cfg *Config
|
||||
|
||||
width int
|
||||
|
||||
bck *runeBufferBck
|
||||
|
||||
offset string
|
||||
|
||||
lastKill []rune
|
||||
|
||||
sync.Mutex
|
||||
}
|
||||
|
||||
func (r* RuneBuffer) pushKill(text []rune) {
|
||||
r.lastKill = append([]rune{}, text...)
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) OnWidthChange(newWidth int) {
|
||||
r.Lock()
|
||||
r.width = newWidth
|
||||
r.Unlock()
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) Backup() {
|
||||
r.Lock()
|
||||
r.bck = &runeBufferBck{r.buf, r.idx}
|
||||
r.Unlock()
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) Restore() {
|
||||
r.Refresh(func() {
|
||||
if r.bck == nil {
|
||||
return
|
||||
}
|
||||
r.buf = r.bck.buf
|
||||
r.idx = r.bck.idx
|
||||
})
|
||||
}
|
||||
|
||||
func NewRuneBuffer(w io.Writer, prompt string, cfg *Config, width int) *RuneBuffer {
|
||||
rb := &RuneBuffer{
|
||||
w: w,
|
||||
interactive: cfg.useInteractive(),
|
||||
cfg: cfg,
|
||||
width: width,
|
||||
}
|
||||
rb.SetPrompt(prompt)
|
||||
return rb
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) SetConfig(cfg *Config) {
|
||||
r.Lock()
|
||||
r.cfg = cfg
|
||||
r.interactive = cfg.useInteractive()
|
||||
r.Unlock()
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) SetMask(m rune) {
|
||||
r.Lock()
|
||||
r.cfg.MaskRune = m
|
||||
r.Unlock()
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) CurrentWidth(x int) int {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
return runes.WidthAll(r.buf[:x])
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) PromptLen() int {
|
||||
r.Lock()
|
||||
width := r.promptLen()
|
||||
r.Unlock()
|
||||
return width
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) promptLen() int {
|
||||
return runes.WidthAll(runes.ColorFilter(r.prompt))
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) RuneSlice(i int) []rune {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
if i > 0 {
|
||||
rs := make([]rune, i)
|
||||
copy(rs, r.buf[r.idx:r.idx+i])
|
||||
return rs
|
||||
}
|
||||
rs := make([]rune, -i)
|
||||
copy(rs, r.buf[r.idx+i:r.idx])
|
||||
return rs
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) Runes() []rune {
|
||||
r.Lock()
|
||||
newr := make([]rune, len(r.buf))
|
||||
copy(newr, r.buf)
|
||||
r.Unlock()
|
||||
return newr
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) Pos() int {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
return r.idx
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) Len() int {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
return len(r.buf)
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) MoveToLineStart() {
|
||||
r.Refresh(func() {
|
||||
if r.idx == 0 {
|
||||
return
|
||||
}
|
||||
r.idx = 0
|
||||
})
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) MoveBackward() {
|
||||
r.Refresh(func() {
|
||||
if r.idx == 0 {
|
||||
return
|
||||
}
|
||||
r.idx--
|
||||
})
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) WriteString(s string) {
|
||||
r.WriteRunes([]rune(s))
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) WriteRune(s rune) {
|
||||
r.WriteRunes([]rune{s})
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) WriteRunes(s []rune) {
|
||||
r.Refresh(func() {
|
||||
tail := append(s, r.buf[r.idx:]...)
|
||||
r.buf = append(r.buf[:r.idx], tail...)
|
||||
r.idx += len(s)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) MoveForward() {
|
||||
r.Refresh(func() {
|
||||
if r.idx == len(r.buf) {
|
||||
return
|
||||
}
|
||||
r.idx++
|
||||
})
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) IsCursorInEnd() bool {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
return r.idx == len(r.buf)
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) Replace(ch rune) {
|
||||
r.Refresh(func() {
|
||||
r.buf[r.idx] = ch
|
||||
})
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) Erase() {
|
||||
r.Refresh(func() {
|
||||
r.idx = 0
|
||||
r.pushKill(r.buf[:])
|
||||
r.buf = r.buf[:0]
|
||||
})
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) Delete() (success bool) {
|
||||
r.Refresh(func() {
|
||||
if r.idx == len(r.buf) {
|
||||
return
|
||||
}
|
||||
r.pushKill(r.buf[r.idx : r.idx+1])
|
||||
r.buf = append(r.buf[:r.idx], r.buf[r.idx+1:]...)
|
||||
success = true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) DeleteWord() {
|
||||
if r.idx == len(r.buf) {
|
||||
return
|
||||
}
|
||||
init := r.idx
|
||||
for init < len(r.buf) && IsWordBreak(r.buf[init]) {
|
||||
init++
|
||||
}
|
||||
for i := init + 1; i < len(r.buf); i++ {
|
||||
if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) {
|
||||
r.pushKill(r.buf[r.idx:i-1])
|
||||
r.Refresh(func() {
|
||||
r.buf = append(r.buf[:r.idx], r.buf[i-1:]...)
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
r.Kill()
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) MoveToPrevWord() (success bool) {
|
||||
r.Refresh(func() {
|
||||
if r.idx == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
for i := r.idx - 1; i > 0; i-- {
|
||||
if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) {
|
||||
r.idx = i
|
||||
success = true
|
||||
return
|
||||
}
|
||||
}
|
||||
r.idx = 0
|
||||
success = true
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) KillFront() {
|
||||
r.Refresh(func() {
|
||||
if r.idx == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
length := len(r.buf) - r.idx
|
||||
r.pushKill(r.buf[:r.idx])
|
||||
copy(r.buf[:length], r.buf[r.idx:])
|
||||
r.idx = 0
|
||||
r.buf = r.buf[:length]
|
||||
})
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) Kill() {
|
||||
r.Refresh(func() {
|
||||
r.pushKill(r.buf[r.idx:])
|
||||
r.buf = r.buf[:r.idx]
|
||||
})
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) Transpose() {
|
||||
r.Refresh(func() {
|
||||
if len(r.buf) == 1 {
|
||||
r.idx++
|
||||
}
|
||||
|
||||
if len(r.buf) < 2 {
|
||||
return
|
||||
}
|
||||
|
||||
if r.idx == 0 {
|
||||
r.idx = 1
|
||||
} else if r.idx >= len(r.buf) {
|
||||
r.idx = len(r.buf) - 1
|
||||
}
|
||||
r.buf[r.idx], r.buf[r.idx-1] = r.buf[r.idx-1], r.buf[r.idx]
|
||||
r.idx++
|
||||
})
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) MoveToNextWord() {
|
||||
r.Refresh(func() {
|
||||
for i := r.idx + 1; i < len(r.buf); i++ {
|
||||
if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) {
|
||||
r.idx = i
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
r.idx = len(r.buf)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) MoveToEndWord() {
|
||||
r.Refresh(func() {
|
||||
// already at the end, so do nothing
|
||||
if r.idx == len(r.buf) {
|
||||
return
|
||||
}
|
||||
// if we are at the end of a word already, go to next
|
||||
if !IsWordBreak(r.buf[r.idx]) && IsWordBreak(r.buf[r.idx+1]) {
|
||||
r.idx++
|
||||
}
|
||||
|
||||
// keep going until at the end of a word
|
||||
for i := r.idx + 1; i < len(r.buf); i++ {
|
||||
if IsWordBreak(r.buf[i]) && !IsWordBreak(r.buf[i-1]) {
|
||||
r.idx = i - 1
|
||||
return
|
||||
}
|
||||
}
|
||||
r.idx = len(r.buf)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) BackEscapeWord() {
|
||||
r.Refresh(func() {
|
||||
if r.idx == 0 {
|
||||
return
|
||||
}
|
||||
for i := r.idx - 1; i > 0; i-- {
|
||||
if !IsWordBreak(r.buf[i]) && IsWordBreak(r.buf[i-1]) {
|
||||
r.pushKill(r.buf[i:r.idx])
|
||||
r.buf = append(r.buf[:i], r.buf[r.idx:]...)
|
||||
r.idx = i
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
r.buf = r.buf[:0]
|
||||
r.idx = 0
|
||||
})
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) Yank() {
|
||||
if len(r.lastKill) == 0 {
|
||||
return
|
||||
}
|
||||
r.Refresh(func() {
|
||||
buf := make([]rune, 0, len(r.buf) + len(r.lastKill))
|
||||
buf = append(buf, r.buf[:r.idx]...)
|
||||
buf = append(buf, r.lastKill...)
|
||||
buf = append(buf, r.buf[r.idx:]...)
|
||||
r.buf = buf
|
||||
r.idx += len(r.lastKill)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) Backspace() {
|
||||
r.Refresh(func() {
|
||||
if r.idx == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
r.idx--
|
||||
r.buf = append(r.buf[:r.idx], r.buf[r.idx+1:]...)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) MoveToLineEnd() {
|
||||
r.Refresh(func() {
|
||||
if r.idx == len(r.buf) {
|
||||
return
|
||||
}
|
||||
|
||||
r.idx = len(r.buf)
|
||||
})
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) LineCount(width int) int {
|
||||
if width == -1 {
|
||||
width = r.width
|
||||
}
|
||||
return LineCount(width,
|
||||
runes.WidthAll(r.buf)+r.PromptLen())
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) MoveTo(ch rune, prevChar, reverse bool) (success bool) {
|
||||
r.Refresh(func() {
|
||||
if reverse {
|
||||
for i := r.idx - 1; i >= 0; i-- {
|
||||
if r.buf[i] == ch {
|
||||
r.idx = i
|
||||
if prevChar {
|
||||
r.idx++
|
||||
}
|
||||
success = true
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
for i := r.idx + 1; i < len(r.buf); i++ {
|
||||
if r.buf[i] == ch {
|
||||
r.idx = i
|
||||
if prevChar {
|
||||
r.idx--
|
||||
}
|
||||
success = true
|
||||
return
|
||||
}
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) isInLineEdge() bool {
|
||||
if isWindows {
|
||||
return false
|
||||
}
|
||||
sp := r.getSplitByLine(r.buf)
|
||||
return len(sp[len(sp)-1]) == 0
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) getSplitByLine(rs []rune) []string {
|
||||
return SplitByLine(r.promptLen(), r.width, rs)
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) IdxLine(width int) int {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
return r.idxLine(width)
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) idxLine(width int) int {
|
||||
if width == 0 {
|
||||
return 0
|
||||
}
|
||||
sp := r.getSplitByLine(r.buf[:r.idx])
|
||||
return len(sp) - 1
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) CursorLineCount() int {
|
||||
return r.LineCount(r.width) - r.IdxLine(r.width)
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) Refresh(f func()) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
if !r.interactive {
|
||||
if f != nil {
|
||||
f()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
r.clean()
|
||||
if f != nil {
|
||||
f()
|
||||
}
|
||||
r.print()
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) SetOffset(offset string) {
|
||||
r.Lock()
|
||||
r.offset = offset
|
||||
r.Unlock()
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) print() {
|
||||
r.w.Write(r.output())
|
||||
r.hadClean = false
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) output() []byte {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
buf.WriteString(string(r.prompt))
|
||||
if r.cfg.EnableMask && len(r.buf) > 0 {
|
||||
buf.Write([]byte(strings.Repeat(string(r.cfg.MaskRune), len(r.buf)-1)))
|
||||
if r.buf[len(r.buf)-1] == '\n' {
|
||||
buf.Write([]byte{'\n'})
|
||||
} else {
|
||||
buf.Write([]byte(string(r.cfg.MaskRune)))
|
||||
}
|
||||
if len(r.buf) > r.idx {
|
||||
buf.Write(r.getBackspaceSequence())
|
||||
}
|
||||
|
||||
} else {
|
||||
for _, e := range r.cfg.Painter.Paint(r.buf, r.idx) {
|
||||
if e == '\t' {
|
||||
buf.WriteString(strings.Repeat(" ", TabWidth))
|
||||
} else {
|
||||
buf.WriteRune(e)
|
||||
}
|
||||
}
|
||||
if r.isInLineEdge() {
|
||||
buf.Write([]byte(" \b"))
|
||||
}
|
||||
}
|
||||
// cursor position
|
||||
if len(r.buf) > r.idx {
|
||||
buf.Write(r.getBackspaceSequence())
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) getBackspaceSequence() []byte {
|
||||
var sep = map[int]bool{}
|
||||
|
||||
var i int
|
||||
for {
|
||||
if i >= runes.WidthAll(r.buf) {
|
||||
break
|
||||
}
|
||||
|
||||
if i == 0 {
|
||||
i -= r.promptLen()
|
||||
}
|
||||
i += r.width
|
||||
|
||||
sep[i] = true
|
||||
}
|
||||
var buf []byte
|
||||
for i := len(r.buf); i > r.idx; i-- {
|
||||
// move input to the left of one
|
||||
buf = append(buf, '\b')
|
||||
if sep[i] {
|
||||
// up one line, go to the start of the line and move cursor right to the end (r.width)
|
||||
buf = append(buf, "\033[A\r"+"\033["+strconv.Itoa(r.width)+"C"...)
|
||||
}
|
||||
}
|
||||
|
||||
return buf
|
||||
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) Reset() []rune {
|
||||
ret := runes.Copy(r.buf)
|
||||
r.buf = r.buf[:0]
|
||||
r.idx = 0
|
||||
return ret
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) calWidth(m int) int {
|
||||
if m > 0 {
|
||||
return runes.WidthAll(r.buf[r.idx : r.idx+m])
|
||||
}
|
||||
return runes.WidthAll(r.buf[r.idx+m : r.idx])
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) SetStyle(start, end int, style string) {
|
||||
if end < start {
|
||||
panic("end < start")
|
||||
}
|
||||
|
||||
// goto start
|
||||
move := start - r.idx
|
||||
if move > 0 {
|
||||
r.w.Write([]byte(string(r.buf[r.idx : r.idx+move])))
|
||||
} else {
|
||||
r.w.Write(bytes.Repeat([]byte("\b"), r.calWidth(move)))
|
||||
}
|
||||
r.w.Write([]byte("\033[" + style + "m"))
|
||||
r.w.Write([]byte(string(r.buf[start:end])))
|
||||
r.w.Write([]byte("\033[0m"))
|
||||
// TODO: move back
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) SetWithIdx(idx int, buf []rune) {
|
||||
r.Refresh(func() {
|
||||
r.buf = buf
|
||||
r.idx = idx
|
||||
})
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) Set(buf []rune) {
|
||||
r.SetWithIdx(len(buf), buf)
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) SetPrompt(prompt string) {
|
||||
r.Lock()
|
||||
r.prompt = []rune(prompt)
|
||||
r.Unlock()
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) cleanOutput(w io.Writer, idxLine int) {
|
||||
buf := bufio.NewWriter(w)
|
||||
|
||||
if r.width == 0 {
|
||||
buf.WriteString(strings.Repeat("\r\b", len(r.buf)+r.promptLen()))
|
||||
buf.Write([]byte("\033[J"))
|
||||
} else {
|
||||
buf.Write([]byte("\033[J")) // just like ^k :)
|
||||
if idxLine == 0 {
|
||||
buf.WriteString("\033[2K")
|
||||
buf.WriteString("\r")
|
||||
} else {
|
||||
for i := 0; i < idxLine; i++ {
|
||||
io.WriteString(buf, "\033[2K\r\033[A")
|
||||
}
|
||||
io.WriteString(buf, "\033[2K\r")
|
||||
}
|
||||
}
|
||||
buf.Flush()
|
||||
return
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) Clean() {
|
||||
r.Lock()
|
||||
r.clean()
|
||||
r.Unlock()
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) clean() {
|
||||
r.cleanWithIdxLine(r.idxLine(r.width))
|
||||
}
|
||||
|
||||
func (r *RuneBuffer) cleanWithIdxLine(idxLine int) {
|
||||
if r.hadClean || !r.interactive {
|
||||
return
|
||||
}
|
||||
r.hadClean = true
|
||||
r.cleanOutput(r.w, idxLine)
|
||||
}
|
||||
|
|
@ -0,0 +1,223 @@
|
|||
package readline
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
var runes = Runes{}
|
||||
var TabWidth = 4
|
||||
|
||||
type Runes struct{}
|
||||
|
||||
func (Runes) EqualRune(a, b rune, fold bool) bool {
|
||||
if a == b {
|
||||
return true
|
||||
}
|
||||
if !fold {
|
||||
return false
|
||||
}
|
||||
if a > b {
|
||||
a, b = b, a
|
||||
}
|
||||
if b < utf8.RuneSelf && 'A' <= a && a <= 'Z' {
|
||||
if b == a+'a'-'A' {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r Runes) EqualRuneFold(a, b rune) bool {
|
||||
return r.EqualRune(a, b, true)
|
||||
}
|
||||
|
||||
func (r Runes) EqualFold(a, b []rune) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(a); i++ {
|
||||
if r.EqualRuneFold(a[i], b[i]) {
|
||||
continue
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (Runes) Equal(a, b []rune) bool {
|
||||
if len(a) != len(b) {
|
||||
return false
|
||||
}
|
||||
for i := 0; i < len(a); i++ {
|
||||
if a[i] != b[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (rs Runes) IndexAllBckEx(r, sub []rune, fold bool) int {
|
||||
for i := len(r) - len(sub); i >= 0; i-- {
|
||||
found := true
|
||||
for j := 0; j < len(sub); j++ {
|
||||
if !rs.EqualRune(r[i+j], sub[j], fold) {
|
||||
found = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if found {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// Search in runes from end to front
|
||||
func (rs Runes) IndexAllBck(r, sub []rune) int {
|
||||
return rs.IndexAllBckEx(r, sub, false)
|
||||
}
|
||||
|
||||
// Search in runes from front to end
|
||||
func (rs Runes) IndexAll(r, sub []rune) int {
|
||||
return rs.IndexAllEx(r, sub, false)
|
||||
}
|
||||
|
||||
func (rs Runes) IndexAllEx(r, sub []rune, fold bool) int {
|
||||
for i := 0; i < len(r); i++ {
|
||||
found := true
|
||||
if len(r[i:]) < len(sub) {
|
||||
return -1
|
||||
}
|
||||
for j := 0; j < len(sub); j++ {
|
||||
if !rs.EqualRune(r[i+j], sub[j], fold) {
|
||||
found = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if found {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (Runes) Index(r rune, rs []rune) int {
|
||||
for i := 0; i < len(rs); i++ {
|
||||
if rs[i] == r {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (Runes) ColorFilter(r []rune) []rune {
|
||||
newr := make([]rune, 0, len(r))
|
||||
for pos := 0; pos < len(r); pos++ {
|
||||
if r[pos] == '\033' && r[pos+1] == '[' {
|
||||
idx := runes.Index('m', r[pos+2:])
|
||||
if idx == -1 {
|
||||
continue
|
||||
}
|
||||
pos += idx + 2
|
||||
continue
|
||||
}
|
||||
newr = append(newr, r[pos])
|
||||
}
|
||||
return newr
|
||||
}
|
||||
|
||||
var zeroWidth = []*unicode.RangeTable{
|
||||
unicode.Mn,
|
||||
unicode.Me,
|
||||
unicode.Cc,
|
||||
unicode.Cf,
|
||||
}
|
||||
|
||||
var doubleWidth = []*unicode.RangeTable{
|
||||
unicode.Han,
|
||||
unicode.Hangul,
|
||||
unicode.Hiragana,
|
||||
unicode.Katakana,
|
||||
}
|
||||
|
||||
func (Runes) Width(r rune) int {
|
||||
if r == '\t' {
|
||||
return TabWidth
|
||||
}
|
||||
if unicode.IsOneOf(zeroWidth, r) {
|
||||
return 0
|
||||
}
|
||||
if unicode.IsOneOf(doubleWidth, r) {
|
||||
return 2
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func (Runes) WidthAll(r []rune) (length int) {
|
||||
for i := 0; i < len(r); i++ {
|
||||
length += runes.Width(r[i])
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (Runes) Backspace(r []rune) []byte {
|
||||
return bytes.Repeat([]byte{'\b'}, runes.WidthAll(r))
|
||||
}
|
||||
|
||||
func (Runes) Copy(r []rune) []rune {
|
||||
n := make([]rune, len(r))
|
||||
copy(n, r)
|
||||
return n
|
||||
}
|
||||
|
||||
func (Runes) HasPrefixFold(r, prefix []rune) bool {
|
||||
if len(r) < len(prefix) {
|
||||
return false
|
||||
}
|
||||
return runes.EqualFold(r[:len(prefix)], prefix)
|
||||
}
|
||||
|
||||
func (Runes) HasPrefix(r, prefix []rune) bool {
|
||||
if len(r) < len(prefix) {
|
||||
return false
|
||||
}
|
||||
return runes.Equal(r[:len(prefix)], prefix)
|
||||
}
|
||||
|
||||
func (Runes) Aggregate(candicate [][]rune) (same []rune, size int) {
|
||||
for i := 0; i < len(candicate[0]); i++ {
|
||||
for j := 0; j < len(candicate)-1; j++ {
|
||||
if i >= len(candicate[j]) || i >= len(candicate[j+1]) {
|
||||
goto aggregate
|
||||
}
|
||||
if candicate[j][i] != candicate[j+1][i] {
|
||||
goto aggregate
|
||||
}
|
||||
}
|
||||
size = i + 1
|
||||
}
|
||||
aggregate:
|
||||
if size > 0 {
|
||||
same = runes.Copy(candicate[0][:size])
|
||||
for i := 0; i < len(candicate); i++ {
|
||||
n := runes.Copy(candicate[i])
|
||||
copy(n, n[size:])
|
||||
candicate[i] = n[:len(n)-size]
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (Runes) TrimSpaceLeft(in []rune) []rune {
|
||||
firstIndex := len(in)
|
||||
for i, r := range in {
|
||||
if unicode.IsSpace(r) == false {
|
||||
firstIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
return in[firstIndex:]
|
||||
}
|
||||
|
|
@ -0,0 +1,164 @@
|
|||
package readline
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"container/list"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
S_STATE_FOUND = iota
|
||||
S_STATE_FAILING
|
||||
)
|
||||
|
||||
const (
|
||||
S_DIR_BCK = iota
|
||||
S_DIR_FWD
|
||||
)
|
||||
|
||||
type opSearch struct {
|
||||
inMode bool
|
||||
state int
|
||||
dir int
|
||||
source *list.Element
|
||||
w io.Writer
|
||||
buf *RuneBuffer
|
||||
data []rune
|
||||
history *opHistory
|
||||
cfg *Config
|
||||
markStart int
|
||||
markEnd int
|
||||
width int
|
||||
}
|
||||
|
||||
func newOpSearch(w io.Writer, buf *RuneBuffer, history *opHistory, cfg *Config, width int) *opSearch {
|
||||
return &opSearch{
|
||||
w: w,
|
||||
buf: buf,
|
||||
cfg: cfg,
|
||||
history: history,
|
||||
width: width,
|
||||
}
|
||||
}
|
||||
|
||||
func (o *opSearch) OnWidthChange(newWidth int) {
|
||||
o.width = newWidth
|
||||
}
|
||||
|
||||
func (o *opSearch) IsSearchMode() bool {
|
||||
return o.inMode
|
||||
}
|
||||
|
||||
func (o *opSearch) SearchBackspace() {
|
||||
if len(o.data) > 0 {
|
||||
o.data = o.data[:len(o.data)-1]
|
||||
o.search(true)
|
||||
}
|
||||
}
|
||||
|
||||
func (o *opSearch) findHistoryBy(isNewSearch bool) (int, *list.Element) {
|
||||
if o.dir == S_DIR_BCK {
|
||||
return o.history.FindBck(isNewSearch, o.data, o.buf.idx)
|
||||
}
|
||||
return o.history.FindFwd(isNewSearch, o.data, o.buf.idx)
|
||||
}
|
||||
|
||||
func (o *opSearch) search(isChange bool) bool {
|
||||
if len(o.data) == 0 {
|
||||
o.state = S_STATE_FOUND
|
||||
o.SearchRefresh(-1)
|
||||
return true
|
||||
}
|
||||
idx, elem := o.findHistoryBy(isChange)
|
||||
if elem == nil {
|
||||
o.SearchRefresh(-2)
|
||||
return false
|
||||
}
|
||||
o.history.current = elem
|
||||
|
||||
item := o.history.showItem(o.history.current.Value)
|
||||
start, end := 0, 0
|
||||
if o.dir == S_DIR_BCK {
|
||||
start, end = idx, idx+len(o.data)
|
||||
} else {
|
||||
start, end = idx, idx+len(o.data)
|
||||
idx += len(o.data)
|
||||
}
|
||||
o.buf.SetWithIdx(idx, item)
|
||||
o.markStart, o.markEnd = start, end
|
||||
o.SearchRefresh(idx)
|
||||
return true
|
||||
}
|
||||
|
||||
func (o *opSearch) SearchChar(r rune) {
|
||||
o.data = append(o.data, r)
|
||||
o.search(true)
|
||||
}
|
||||
|
||||
func (o *opSearch) SearchMode(dir int) bool {
|
||||
if o.width == 0 {
|
||||
return false
|
||||
}
|
||||
alreadyInMode := o.inMode
|
||||
o.inMode = true
|
||||
o.dir = dir
|
||||
o.source = o.history.current
|
||||
if alreadyInMode {
|
||||
o.search(false)
|
||||
} else {
|
||||
o.SearchRefresh(-1)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (o *opSearch) ExitSearchMode(revert bool) {
|
||||
if revert {
|
||||
o.history.current = o.source
|
||||
o.buf.Set(o.history.showItem(o.history.current.Value))
|
||||
}
|
||||
o.markStart, o.markEnd = 0, 0
|
||||
o.state = S_STATE_FOUND
|
||||
o.inMode = false
|
||||
o.source = nil
|
||||
o.data = nil
|
||||
}
|
||||
|
||||
func (o *opSearch) SearchRefresh(x int) {
|
||||
if x == -2 {
|
||||
o.state = S_STATE_FAILING
|
||||
} else if x >= 0 {
|
||||
o.state = S_STATE_FOUND
|
||||
}
|
||||
if x < 0 {
|
||||
x = o.buf.idx
|
||||
}
|
||||
x = o.buf.CurrentWidth(x)
|
||||
x += o.buf.PromptLen()
|
||||
x = x % o.width
|
||||
|
||||
if o.markStart > 0 {
|
||||
o.buf.SetStyle(o.markStart, o.markEnd, "4")
|
||||
}
|
||||
|
||||
lineCnt := o.buf.CursorLineCount()
|
||||
buf := bytes.NewBuffer(nil)
|
||||
buf.Write(bytes.Repeat([]byte("\n"), lineCnt))
|
||||
buf.WriteString("\033[J")
|
||||
if o.state == S_STATE_FAILING {
|
||||
buf.WriteString("failing ")
|
||||
}
|
||||
if o.dir == S_DIR_BCK {
|
||||
buf.WriteString("bck")
|
||||
} else if o.dir == S_DIR_FWD {
|
||||
buf.WriteString("fwd")
|
||||
}
|
||||
buf.WriteString("-i-search: ")
|
||||
buf.WriteString(string(o.data)) // keyword
|
||||
buf.WriteString("\033[4m \033[0m") // _
|
||||
fmt.Fprintf(buf, "\r\033[%dA", lineCnt) // move prev
|
||||
if x > 0 {
|
||||
fmt.Fprintf(buf, "\033[%dC", x) // move forward
|
||||
}
|
||||
o.w.Write(buf.Bytes())
|
||||
}
|
||||
|
|
@ -0,0 +1,197 @@
|
|||
package readline
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
var (
|
||||
Stdin io.ReadCloser = os.Stdin
|
||||
Stdout io.WriteCloser = os.Stdout
|
||||
Stderr io.WriteCloser = os.Stderr
|
||||
)
|
||||
|
||||
var (
|
||||
std *Instance
|
||||
stdOnce sync.Once
|
||||
)
|
||||
|
||||
// global instance will not submit history automatic
|
||||
func getInstance() *Instance {
|
||||
stdOnce.Do(func() {
|
||||
std, _ = NewEx(&Config{
|
||||
DisableAutoSaveHistory: true,
|
||||
})
|
||||
})
|
||||
return std
|
||||
}
|
||||
|
||||
// let readline load history from filepath
|
||||
// and try to persist history into disk
|
||||
// set fp to "" to prevent readline persisting history to disk
|
||||
// so the `AddHistory` will return nil error forever.
|
||||
func SetHistoryPath(fp string) {
|
||||
ins := getInstance()
|
||||
cfg := ins.Config.Clone()
|
||||
cfg.HistoryFile = fp
|
||||
ins.SetConfig(cfg)
|
||||
}
|
||||
|
||||
// set auto completer to global instance
|
||||
func SetAutoComplete(completer AutoCompleter) {
|
||||
ins := getInstance()
|
||||
cfg := ins.Config.Clone()
|
||||
cfg.AutoComplete = completer
|
||||
ins.SetConfig(cfg)
|
||||
}
|
||||
|
||||
// add history to global instance manually
|
||||
// raise error only if `SetHistoryPath` is set with a non-empty path
|
||||
func AddHistory(content string) error {
|
||||
ins := getInstance()
|
||||
return ins.SaveHistory(content)
|
||||
}
|
||||
|
||||
func Password(prompt string) ([]byte, error) {
|
||||
ins := getInstance()
|
||||
return ins.ReadPassword(prompt)
|
||||
}
|
||||
|
||||
// readline with global configs
|
||||
func Line(prompt string) (string, error) {
|
||||
ins := getInstance()
|
||||
ins.SetPrompt(prompt)
|
||||
return ins.Readline()
|
||||
}
|
||||
|
||||
type CancelableStdin struct {
|
||||
r io.Reader
|
||||
mutex sync.Mutex
|
||||
stop chan struct{}
|
||||
closed int32
|
||||
notify chan struct{}
|
||||
data []byte
|
||||
read int
|
||||
err error
|
||||
}
|
||||
|
||||
func NewCancelableStdin(r io.Reader) *CancelableStdin {
|
||||
c := &CancelableStdin{
|
||||
r: r,
|
||||
notify: make(chan struct{}),
|
||||
stop: make(chan struct{}),
|
||||
}
|
||||
go c.ioloop()
|
||||
return c
|
||||
}
|
||||
|
||||
func (c *CancelableStdin) ioloop() {
|
||||
loop:
|
||||
for {
|
||||
select {
|
||||
case <-c.notify:
|
||||
c.read, c.err = c.r.Read(c.data)
|
||||
select {
|
||||
case c.notify <- struct{}{}:
|
||||
case <-c.stop:
|
||||
break loop
|
||||
}
|
||||
case <-c.stop:
|
||||
break loop
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CancelableStdin) Read(b []byte) (n int, err error) {
|
||||
c.mutex.Lock()
|
||||
defer c.mutex.Unlock()
|
||||
if atomic.LoadInt32(&c.closed) == 1 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
c.data = b
|
||||
select {
|
||||
case c.notify <- struct{}{}:
|
||||
case <-c.stop:
|
||||
return 0, io.EOF
|
||||
}
|
||||
select {
|
||||
case <-c.notify:
|
||||
return c.read, c.err
|
||||
case <-c.stop:
|
||||
return 0, io.EOF
|
||||
}
|
||||
}
|
||||
|
||||
func (c *CancelableStdin) Close() error {
|
||||
if atomic.CompareAndSwapInt32(&c.closed, 0, 1) {
|
||||
close(c.stop)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FillableStdin is a stdin reader which can prepend some data before
|
||||
// reading into the real stdin
|
||||
type FillableStdin struct {
|
||||
sync.Mutex
|
||||
stdin io.Reader
|
||||
stdinBuffer io.ReadCloser
|
||||
buf []byte
|
||||
bufErr error
|
||||
}
|
||||
|
||||
// NewFillableStdin gives you FillableStdin
|
||||
func NewFillableStdin(stdin io.Reader) (io.ReadCloser, io.Writer) {
|
||||
r, w := io.Pipe()
|
||||
s := &FillableStdin{
|
||||
stdinBuffer: r,
|
||||
stdin: stdin,
|
||||
}
|
||||
s.ioloop()
|
||||
return s, w
|
||||
}
|
||||
|
||||
func (s *FillableStdin) ioloop() {
|
||||
go func() {
|
||||
for {
|
||||
bufR := make([]byte, 100)
|
||||
var n int
|
||||
n, s.bufErr = s.stdinBuffer.Read(bufR)
|
||||
if s.bufErr != nil {
|
||||
if s.bufErr == io.ErrClosedPipe {
|
||||
break
|
||||
}
|
||||
}
|
||||
s.Lock()
|
||||
s.buf = append(s.buf, bufR[:n]...)
|
||||
s.Unlock()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
// Read will read from the local buffer and if no data, read from stdin
|
||||
func (s *FillableStdin) Read(p []byte) (n int, err error) {
|
||||
s.Lock()
|
||||
i := len(s.buf)
|
||||
if len(p) < i {
|
||||
i = len(p)
|
||||
}
|
||||
if i > 0 {
|
||||
n := copy(p, s.buf)
|
||||
s.buf = s.buf[:0]
|
||||
cerr := s.bufErr
|
||||
s.bufErr = nil
|
||||
s.Unlock()
|
||||
return n, cerr
|
||||
}
|
||||
s.Unlock()
|
||||
n, err = s.stdin.Read(p)
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (s *FillableStdin) Close() error {
|
||||
s.stdinBuffer.Close()
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
// +build windows
|
||||
|
||||
package readline
|
||||
|
||||
func init() {
|
||||
Stdin = NewRawReader()
|
||||
Stdout = NewANSIWriter(Stdout)
|
||||
Stderr = NewANSIWriter(Stderr)
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin dragonfly freebsd linux,!appengine netbsd openbsd solaris
|
||||
|
||||
// Package terminal provides support functions for dealing with terminals, as
|
||||
// commonly found on UNIX systems.
|
||||
//
|
||||
// Putting a terminal into raw mode is the most common requirement:
|
||||
//
|
||||
// oldState, err := terminal.MakeRaw(0)
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
// defer terminal.Restore(0, oldState)
|
||||
package readline
|
||||
|
||||
import (
|
||||
"io"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// State contains the state of a terminal.
|
||||
type State struct {
|
||||
termios Termios
|
||||
}
|
||||
|
||||
// IsTerminal returns true if the given file descriptor is a terminal.
|
||||
func IsTerminal(fd int) bool {
|
||||
_, err := getTermios(fd)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// MakeRaw put the terminal connected to the given file descriptor into raw
|
||||
// mode and returns the previous state of the terminal so that it can be
|
||||
// restored.
|
||||
func MakeRaw(fd int) (*State, error) {
|
||||
var oldState State
|
||||
|
||||
if termios, err := getTermios(fd); err != nil {
|
||||
return nil, err
|
||||
} else {
|
||||
oldState.termios = *termios
|
||||
}
|
||||
|
||||
newState := oldState.termios
|
||||
// This attempts to replicate the behaviour documented for cfmakeraw in
|
||||
// the termios(3) manpage.
|
||||
newState.Iflag &^= syscall.IGNBRK | syscall.BRKINT | syscall.PARMRK | syscall.ISTRIP | syscall.INLCR | syscall.IGNCR | syscall.ICRNL | syscall.IXON
|
||||
// newState.Oflag &^= syscall.OPOST
|
||||
newState.Lflag &^= syscall.ECHO | syscall.ECHONL | syscall.ICANON | syscall.ISIG | syscall.IEXTEN
|
||||
newState.Cflag &^= syscall.CSIZE | syscall.PARENB
|
||||
newState.Cflag |= syscall.CS8
|
||||
|
||||
newState.Cc[syscall.VMIN] = 1
|
||||
newState.Cc[syscall.VTIME] = 0
|
||||
|
||||
return &oldState, setTermios(fd, &newState)
|
||||
}
|
||||
|
||||
// GetState returns the current state of a terminal which may be useful to
|
||||
// restore the terminal after a signal.
|
||||
func GetState(fd int) (*State, error) {
|
||||
termios, err := getTermios(fd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &State{termios: *termios}, nil
|
||||
}
|
||||
|
||||
// Restore restores the terminal connected to the given file descriptor to a
|
||||
// previous state.
|
||||
func restoreTerm(fd int, state *State) error {
|
||||
return setTermios(fd, &state.termios)
|
||||
}
|
||||
|
||||
// ReadPassword reads a line of input from a terminal without local echo. This
|
||||
// is commonly used for inputting passwords and other sensitive data. The slice
|
||||
// returned does not include the \n.
|
||||
func ReadPassword(fd int) ([]byte, error) {
|
||||
oldState, err := getTermios(fd)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
newState := oldState
|
||||
newState.Lflag &^= syscall.ECHO
|
||||
newState.Lflag |= syscall.ICANON | syscall.ISIG
|
||||
newState.Iflag |= syscall.ICRNL
|
||||
if err := setTermios(fd, newState); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
setTermios(fd, oldState)
|
||||
}()
|
||||
|
||||
var buf [16]byte
|
||||
var ret []byte
|
||||
for {
|
||||
n, err := syscall.Read(fd, buf[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n == 0 {
|
||||
if len(ret) == 0 {
|
||||
return nil, io.EOF
|
||||
}
|
||||
break
|
||||
}
|
||||
if buf[n-1] == '\n' {
|
||||
n--
|
||||
}
|
||||
ret = append(ret, buf[:n]...)
|
||||
if n < len(buf) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin dragonfly freebsd netbsd openbsd
|
||||
|
||||
package readline
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
func getTermios(fd int) (*Termios, error) {
|
||||
termios := new(Termios)
|
||||
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), syscall.TIOCGETA, uintptr(unsafe.Pointer(termios)), 0, 0, 0)
|
||||
if err != 0 {
|
||||
return nil, err
|
||||
}
|
||||
return termios, nil
|
||||
}
|
||||
|
||||
func setTermios(fd int, termios *Termios) error {
|
||||
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), syscall.TIOCSETA, uintptr(unsafe.Pointer(termios)), 0, 0, 0)
|
||||
if err != 0 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package readline
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// These constants are declared here, rather than importing
|
||||
// them from the syscall package as some syscall packages, even
|
||||
// on linux, for example gccgo, do not declare them.
|
||||
const ioctlReadTermios = 0x5401 // syscall.TCGETS
|
||||
const ioctlWriteTermios = 0x5402 // syscall.TCSETS
|
||||
|
||||
func getTermios(fd int) (*Termios, error) {
|
||||
termios := new(Termios)
|
||||
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlReadTermios, uintptr(unsafe.Pointer(termios)), 0, 0, 0)
|
||||
if err != 0 {
|
||||
return nil, err
|
||||
}
|
||||
return termios, nil
|
||||
}
|
||||
|
||||
func setTermios(fd int, termios *Termios) error {
|
||||
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), ioctlWriteTermios, uintptr(unsafe.Pointer(termios)), 0, 0, 0)
|
||||
if err != 0 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright 2013 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build solaris
|
||||
|
||||
package readline
|
||||
|
||||
import "golang.org/x/sys/unix"
|
||||
|
||||
// GetSize returns the dimensions of the given terminal.
|
||||
func GetSize(fd int) (int, int, error) {
|
||||
ws, err := unix.IoctlGetWinsize(fd, unix.TIOCGWINSZ)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return int(ws.Col), int(ws.Row), nil
|
||||
}
|
||||
|
||||
type Termios unix.Termios
|
||||
|
||||
func getTermios(fd int) (*Termios, error) {
|
||||
termios, err := unix.IoctlGetTermios(fd, unix.TCGETS)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return (*Termios)(termios), nil
|
||||
}
|
||||
|
||||
func setTermios(fd int, termios *Termios) error {
|
||||
return unix.IoctlSetTermios(fd, unix.TCSETSF, (*unix.Termios)(termios))
|
||||
}
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build darwin dragonfly freebsd linux,!appengine netbsd openbsd
|
||||
|
||||
package readline
|
||||
|
||||
import (
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type Termios syscall.Termios
|
||||
|
||||
// GetSize returns the dimensions of the given terminal.
|
||||
func GetSize(fd int) (int, int, error) {
|
||||
var dimensions [4]uint16
|
||||
_, _, err := syscall.Syscall6(syscall.SYS_IOCTL, uintptr(fd), uintptr(syscall.TIOCGWINSZ), uintptr(unsafe.Pointer(&dimensions)), 0, 0, 0)
|
||||
if err != 0 {
|
||||
return 0, 0, err
|
||||
}
|
||||
return int(dimensions[1]), int(dimensions[0]), nil
|
||||
}
|
||||
|
|
@ -0,0 +1,171 @@
|
|||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build windows
|
||||
|
||||
// Package terminal provides support functions for dealing with terminals, as
|
||||
// commonly found on UNIX systems.
|
||||
//
|
||||
// Putting a terminal into raw mode is the most common requirement:
|
||||
//
|
||||
// oldState, err := terminal.MakeRaw(0)
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
// defer terminal.Restore(0, oldState)
|
||||
package readline
|
||||
|
||||
import (
|
||||
"io"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
enableLineInput = 2
|
||||
enableEchoInput = 4
|
||||
enableProcessedInput = 1
|
||||
enableWindowInput = 8
|
||||
enableMouseInput = 16
|
||||
enableInsertMode = 32
|
||||
enableQuickEditMode = 64
|
||||
enableExtendedFlags = 128
|
||||
enableAutoPosition = 256
|
||||
enableProcessedOutput = 1
|
||||
enableWrapAtEolOutput = 2
|
||||
)
|
||||
|
||||
var kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
|
||||
var (
|
||||
procGetConsoleMode = kernel32.NewProc("GetConsoleMode")
|
||||
procSetConsoleMode = kernel32.NewProc("SetConsoleMode")
|
||||
procGetConsoleScreenBufferInfo = kernel32.NewProc("GetConsoleScreenBufferInfo")
|
||||
)
|
||||
|
||||
type (
|
||||
coord struct {
|
||||
x short
|
||||
y short
|
||||
}
|
||||
smallRect struct {
|
||||
left short
|
||||
top short
|
||||
right short
|
||||
bottom short
|
||||
}
|
||||
consoleScreenBufferInfo struct {
|
||||
size coord
|
||||
cursorPosition coord
|
||||
attributes word
|
||||
window smallRect
|
||||
maximumWindowSize coord
|
||||
}
|
||||
)
|
||||
|
||||
type State struct {
|
||||
mode uint32
|
||||
}
|
||||
|
||||
// IsTerminal returns true if the given file descriptor is a terminal.
|
||||
func IsTerminal(fd int) bool {
|
||||
var st uint32
|
||||
r, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0)
|
||||
return r != 0 && e == 0
|
||||
}
|
||||
|
||||
// MakeRaw put the terminal connected to the given file descriptor into raw
|
||||
// mode and returns the previous state of the terminal so that it can be
|
||||
// restored.
|
||||
func MakeRaw(fd int) (*State, error) {
|
||||
var st uint32
|
||||
_, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0)
|
||||
if e != 0 {
|
||||
return nil, error(e)
|
||||
}
|
||||
raw := st &^ (enableEchoInput | enableProcessedInput | enableLineInput | enableProcessedOutput)
|
||||
_, _, e = syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(raw), 0)
|
||||
if e != 0 {
|
||||
return nil, error(e)
|
||||
}
|
||||
return &State{st}, nil
|
||||
}
|
||||
|
||||
// GetState returns the current state of a terminal which may be useful to
|
||||
// restore the terminal after a signal.
|
||||
func GetState(fd int) (*State, error) {
|
||||
var st uint32
|
||||
_, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0)
|
||||
if e != 0 {
|
||||
return nil, error(e)
|
||||
}
|
||||
return &State{st}, nil
|
||||
}
|
||||
|
||||
// Restore restores the terminal connected to the given file descriptor to a
|
||||
// previous state.
|
||||
func restoreTerm(fd int, state *State) error {
|
||||
_, _, err := syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(state.mode), 0)
|
||||
return err
|
||||
}
|
||||
|
||||
// GetSize returns the dimensions of the given terminal.
|
||||
func GetSize(fd int) (width, height int, err error) {
|
||||
var info consoleScreenBufferInfo
|
||||
_, _, e := syscall.Syscall(procGetConsoleScreenBufferInfo.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&info)), 0)
|
||||
if e != 0 {
|
||||
return 0, 0, error(e)
|
||||
}
|
||||
return int(info.size.x), int(info.size.y), nil
|
||||
}
|
||||
|
||||
// ReadPassword reads a line of input from a terminal without local echo. This
|
||||
// is commonly used for inputting passwords and other sensitive data. The slice
|
||||
// returned does not include the \n.
|
||||
func ReadPassword(fd int) ([]byte, error) {
|
||||
var st uint32
|
||||
_, _, e := syscall.Syscall(procGetConsoleMode.Addr(), 2, uintptr(fd), uintptr(unsafe.Pointer(&st)), 0)
|
||||
if e != 0 {
|
||||
return nil, error(e)
|
||||
}
|
||||
old := st
|
||||
|
||||
st &^= (enableEchoInput)
|
||||
st |= (enableProcessedInput | enableLineInput | enableProcessedOutput)
|
||||
_, _, e = syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(st), 0)
|
||||
if e != 0 {
|
||||
return nil, error(e)
|
||||
}
|
||||
|
||||
defer func() {
|
||||
syscall.Syscall(procSetConsoleMode.Addr(), 2, uintptr(fd), uintptr(old), 0)
|
||||
}()
|
||||
|
||||
var buf [16]byte
|
||||
var ret []byte
|
||||
for {
|
||||
n, err := syscall.Read(syscall.Handle(fd), buf[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if n == 0 {
|
||||
if len(ret) == 0 {
|
||||
return nil, io.EOF
|
||||
}
|
||||
break
|
||||
}
|
||||
if buf[n-1] == '\n' {
|
||||
n--
|
||||
}
|
||||
if n > 0 && buf[n-1] == '\r' {
|
||||
n--
|
||||
}
|
||||
ret = append(ret, buf[:n]...)
|
||||
if n < len(buf) {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return ret, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,238 @@
|
|||
package readline
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type Terminal struct {
|
||||
m sync.Mutex
|
||||
cfg *Config
|
||||
outchan chan rune
|
||||
closed int32
|
||||
stopChan chan struct{}
|
||||
kickChan chan struct{}
|
||||
wg sync.WaitGroup
|
||||
isReading int32
|
||||
sleeping int32
|
||||
|
||||
sizeChan chan string
|
||||
}
|
||||
|
||||
func NewTerminal(cfg *Config) (*Terminal, error) {
|
||||
if err := cfg.Init(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t := &Terminal{
|
||||
cfg: cfg,
|
||||
kickChan: make(chan struct{}, 1),
|
||||
outchan: make(chan rune),
|
||||
stopChan: make(chan struct{}, 1),
|
||||
sizeChan: make(chan string, 1),
|
||||
}
|
||||
|
||||
go t.ioloop()
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// SleepToResume will sleep myself, and return only if I'm resumed.
|
||||
func (t *Terminal) SleepToResume() {
|
||||
if !atomic.CompareAndSwapInt32(&t.sleeping, 0, 1) {
|
||||
return
|
||||
}
|
||||
defer atomic.StoreInt32(&t.sleeping, 0)
|
||||
|
||||
t.ExitRawMode()
|
||||
ch := WaitForResume()
|
||||
SuspendMe()
|
||||
<-ch
|
||||
t.EnterRawMode()
|
||||
}
|
||||
|
||||
func (t *Terminal) EnterRawMode() (err error) {
|
||||
return t.cfg.FuncMakeRaw()
|
||||
}
|
||||
|
||||
func (t *Terminal) ExitRawMode() (err error) {
|
||||
return t.cfg.FuncExitRaw()
|
||||
}
|
||||
|
||||
func (t *Terminal) Write(b []byte) (int, error) {
|
||||
return t.cfg.Stdout.Write(b)
|
||||
}
|
||||
|
||||
// WriteStdin prefill the next Stdin fetch
|
||||
// Next time you call ReadLine() this value will be writen before the user input
|
||||
func (t *Terminal) WriteStdin(b []byte) (int, error) {
|
||||
return t.cfg.StdinWriter.Write(b)
|
||||
}
|
||||
|
||||
type termSize struct {
|
||||
left int
|
||||
top int
|
||||
}
|
||||
|
||||
func (t *Terminal) GetOffset(f func(offset string)) {
|
||||
go func() {
|
||||
f(<-t.sizeChan)
|
||||
}()
|
||||
t.Write([]byte("\033[6n"))
|
||||
}
|
||||
|
||||
func (t *Terminal) Print(s string) {
|
||||
fmt.Fprintf(t.cfg.Stdout, "%s", s)
|
||||
}
|
||||
|
||||
func (t *Terminal) PrintRune(r rune) {
|
||||
fmt.Fprintf(t.cfg.Stdout, "%c", r)
|
||||
}
|
||||
|
||||
func (t *Terminal) Readline() *Operation {
|
||||
return NewOperation(t, t.cfg)
|
||||
}
|
||||
|
||||
// return rune(0) if meet EOF
|
||||
func (t *Terminal) ReadRune() rune {
|
||||
ch, ok := <-t.outchan
|
||||
if !ok {
|
||||
return rune(0)
|
||||
}
|
||||
return ch
|
||||
}
|
||||
|
||||
func (t *Terminal) IsReading() bool {
|
||||
return atomic.LoadInt32(&t.isReading) == 1
|
||||
}
|
||||
|
||||
func (t *Terminal) KickRead() {
|
||||
select {
|
||||
case t.kickChan <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Terminal) ioloop() {
|
||||
t.wg.Add(1)
|
||||
defer func() {
|
||||
t.wg.Done()
|
||||
close(t.outchan)
|
||||
}()
|
||||
|
||||
var (
|
||||
isEscape bool
|
||||
isEscapeEx bool
|
||||
expectNextChar bool
|
||||
)
|
||||
|
||||
buf := bufio.NewReader(t.getStdin())
|
||||
for {
|
||||
if !expectNextChar {
|
||||
atomic.StoreInt32(&t.isReading, 0)
|
||||
select {
|
||||
case <-t.kickChan:
|
||||
atomic.StoreInt32(&t.isReading, 1)
|
||||
case <-t.stopChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
expectNextChar = false
|
||||
r, _, err := buf.ReadRune()
|
||||
if err != nil {
|
||||
if strings.Contains(err.Error(), "interrupted system call") {
|
||||
expectNextChar = true
|
||||
continue
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if isEscape {
|
||||
isEscape = false
|
||||
if r == CharEscapeEx {
|
||||
expectNextChar = true
|
||||
isEscapeEx = true
|
||||
continue
|
||||
}
|
||||
r = escapeKey(r, buf)
|
||||
} else if isEscapeEx {
|
||||
isEscapeEx = false
|
||||
if key := readEscKey(r, buf); key != nil {
|
||||
r = escapeExKey(key)
|
||||
// offset
|
||||
if key.typ == 'R' {
|
||||
if _, _, ok := key.Get2(); ok {
|
||||
select {
|
||||
case t.sizeChan <- key.attr:
|
||||
default:
|
||||
}
|
||||
}
|
||||
expectNextChar = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
if r == 0 {
|
||||
expectNextChar = true
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
expectNextChar = true
|
||||
switch r {
|
||||
case CharEsc:
|
||||
if t.cfg.VimMode {
|
||||
t.outchan <- r
|
||||
break
|
||||
}
|
||||
isEscape = true
|
||||
case CharInterrupt, CharEnter, CharCtrlJ, CharDelete:
|
||||
expectNextChar = false
|
||||
fallthrough
|
||||
default:
|
||||
t.outchan <- r
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (t *Terminal) Bell() {
|
||||
fmt.Fprintf(t, "%c", CharBell)
|
||||
}
|
||||
|
||||
func (t *Terminal) Close() error {
|
||||
if atomic.SwapInt32(&t.closed, 1) != 0 {
|
||||
return nil
|
||||
}
|
||||
if closer, ok := t.cfg.Stdin.(io.Closer); ok {
|
||||
closer.Close()
|
||||
}
|
||||
close(t.stopChan)
|
||||
t.wg.Wait()
|
||||
return t.ExitRawMode()
|
||||
}
|
||||
|
||||
func (t *Terminal) GetConfig() *Config {
|
||||
t.m.Lock()
|
||||
cfg := *t.cfg
|
||||
t.m.Unlock()
|
||||
return &cfg
|
||||
}
|
||||
|
||||
func (t *Terminal) getStdin() io.Reader {
|
||||
t.m.Lock()
|
||||
r := t.cfg.Stdin
|
||||
t.m.Unlock()
|
||||
return r
|
||||
}
|
||||
|
||||
func (t *Terminal) SetConfig(c *Config) error {
|
||||
if err := c.Init(); err != nil {
|
||||
return err
|
||||
}
|
||||
t.m.Lock()
|
||||
t.cfg = c
|
||||
t.m.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
|
@ -0,0 +1,277 @@
|
|||
package readline
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"container/list"
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
var (
|
||||
isWindows = false
|
||||
)
|
||||
|
||||
const (
|
||||
CharLineStart = 1
|
||||
CharBackward = 2
|
||||
CharInterrupt = 3
|
||||
CharDelete = 4
|
||||
CharLineEnd = 5
|
||||
CharForward = 6
|
||||
CharBell = 7
|
||||
CharCtrlH = 8
|
||||
CharTab = 9
|
||||
CharCtrlJ = 10
|
||||
CharKill = 11
|
||||
CharCtrlL = 12
|
||||
CharEnter = 13
|
||||
CharNext = 14
|
||||
CharPrev = 16
|
||||
CharBckSearch = 18
|
||||
CharFwdSearch = 19
|
||||
CharTranspose = 20
|
||||
CharCtrlU = 21
|
||||
CharCtrlW = 23
|
||||
CharCtrlY = 25
|
||||
CharCtrlZ = 26
|
||||
CharEsc = 27
|
||||
CharEscapeEx = 91
|
||||
CharBackspace = 127
|
||||
)
|
||||
|
||||
const (
|
||||
MetaBackward rune = -iota - 1
|
||||
MetaForward
|
||||
MetaDelete
|
||||
MetaBackspace
|
||||
MetaTranspose
|
||||
)
|
||||
|
||||
// WaitForResume need to call before current process got suspend.
|
||||
// It will run a ticker until a long duration is occurs,
|
||||
// which means this process is resumed.
|
||||
func WaitForResume() chan struct{} {
|
||||
ch := make(chan struct{})
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
ticker := time.NewTicker(10 * time.Millisecond)
|
||||
t := time.Now()
|
||||
wg.Done()
|
||||
for {
|
||||
now := <-ticker.C
|
||||
if now.Sub(t) > 100*time.Millisecond {
|
||||
break
|
||||
}
|
||||
t = now
|
||||
}
|
||||
ticker.Stop()
|
||||
ch <- struct{}{}
|
||||
}()
|
||||
wg.Wait()
|
||||
return ch
|
||||
}
|
||||
|
||||
func Restore(fd int, state *State) error {
|
||||
err := restoreTerm(fd, state)
|
||||
if err != nil {
|
||||
// errno 0 means everything is ok :)
|
||||
if err.Error() == "errno 0" {
|
||||
return nil
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func IsPrintable(key rune) bool {
|
||||
isInSurrogateArea := key >= 0xd800 && key <= 0xdbff
|
||||
return key >= 32 && !isInSurrogateArea
|
||||
}
|
||||
|
||||
// translate Esc[X
|
||||
func escapeExKey(key *escapeKeyPair) rune {
|
||||
var r rune
|
||||
switch key.typ {
|
||||
case 'D':
|
||||
r = CharBackward
|
||||
case 'C':
|
||||
r = CharForward
|
||||
case 'A':
|
||||
r = CharPrev
|
||||
case 'B':
|
||||
r = CharNext
|
||||
case 'H':
|
||||
r = CharLineStart
|
||||
case 'F':
|
||||
r = CharLineEnd
|
||||
case '~':
|
||||
if key.attr == "3" {
|
||||
r = CharDelete
|
||||
}
|
||||
default:
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
type escapeKeyPair struct {
|
||||
attr string
|
||||
typ rune
|
||||
}
|
||||
|
||||
func (e *escapeKeyPair) Get2() (int, int, bool) {
|
||||
sp := strings.Split(e.attr, ";")
|
||||
if len(sp) < 2 {
|
||||
return -1, -1, false
|
||||
}
|
||||
s1, err := strconv.Atoi(sp[0])
|
||||
if err != nil {
|
||||
return -1, -1, false
|
||||
}
|
||||
s2, err := strconv.Atoi(sp[1])
|
||||
if err != nil {
|
||||
return -1, -1, false
|
||||
}
|
||||
return s1, s2, true
|
||||
}
|
||||
|
||||
func readEscKey(r rune, reader *bufio.Reader) *escapeKeyPair {
|
||||
p := escapeKeyPair{}
|
||||
buf := bytes.NewBuffer(nil)
|
||||
for {
|
||||
if r == ';' {
|
||||
} else if unicode.IsNumber(r) {
|
||||
} else {
|
||||
p.typ = r
|
||||
break
|
||||
}
|
||||
buf.WriteRune(r)
|
||||
r, _, _ = reader.ReadRune()
|
||||
}
|
||||
p.attr = buf.String()
|
||||
return &p
|
||||
}
|
||||
|
||||
// translate EscX to Meta+X
|
||||
func escapeKey(r rune, reader *bufio.Reader) rune {
|
||||
switch r {
|
||||
case 'b':
|
||||
r = MetaBackward
|
||||
case 'f':
|
||||
r = MetaForward
|
||||
case 'd':
|
||||
r = MetaDelete
|
||||
case CharTranspose:
|
||||
r = MetaTranspose
|
||||
case CharBackspace:
|
||||
r = MetaBackspace
|
||||
case 'O':
|
||||
d, _, _ := reader.ReadRune()
|
||||
switch d {
|
||||
case 'H':
|
||||
r = CharLineStart
|
||||
case 'F':
|
||||
r = CharLineEnd
|
||||
default:
|
||||
reader.UnreadRune()
|
||||
}
|
||||
case CharEsc:
|
||||
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func SplitByLine(start, screenWidth int, rs []rune) []string {
|
||||
var ret []string
|
||||
buf := bytes.NewBuffer(nil)
|
||||
currentWidth := start
|
||||
for _, r := range rs {
|
||||
w := runes.Width(r)
|
||||
currentWidth += w
|
||||
buf.WriteRune(r)
|
||||
if currentWidth >= screenWidth {
|
||||
ret = append(ret, buf.String())
|
||||
buf.Reset()
|
||||
currentWidth = 0
|
||||
}
|
||||
}
|
||||
ret = append(ret, buf.String())
|
||||
return ret
|
||||
}
|
||||
|
||||
// calculate how many lines for N character
|
||||
func LineCount(screenWidth, w int) int {
|
||||
r := w / screenWidth
|
||||
if w%screenWidth != 0 {
|
||||
r++
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
func IsWordBreak(i rune) bool {
|
||||
switch {
|
||||
case i >= 'a' && i <= 'z':
|
||||
case i >= 'A' && i <= 'Z':
|
||||
case i >= '0' && i <= '9':
|
||||
default:
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func GetInt(s []string, def int) int {
|
||||
if len(s) == 0 {
|
||||
return def
|
||||
}
|
||||
c, err := strconv.Atoi(s[0])
|
||||
if err != nil {
|
||||
return def
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
type RawMode struct {
|
||||
state *State
|
||||
}
|
||||
|
||||
func (r *RawMode) Enter() (err error) {
|
||||
r.state, err = MakeRaw(GetStdin())
|
||||
return err
|
||||
}
|
||||
|
||||
func (r *RawMode) Exit() error {
|
||||
if r.state == nil {
|
||||
return nil
|
||||
}
|
||||
return Restore(GetStdin(), r.state)
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
func sleep(n int) {
|
||||
Debug(n)
|
||||
time.Sleep(2000 * time.Millisecond)
|
||||
}
|
||||
|
||||
// print a linked list to Debug()
|
||||
func debugList(l *list.List) {
|
||||
idx := 0
|
||||
for e := l.Front(); e != nil; e = e.Next() {
|
||||
Debug(idx, fmt.Sprintf("%+v", e.Value))
|
||||
idx++
|
||||
}
|
||||
}
|
||||
|
||||
// append log info to another file
|
||||
func Debug(o ...interface{}) {
|
||||
f, _ := os.OpenFile("debug.tmp", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
|
||||
fmt.Fprintln(f, o...)
|
||||
f.Close()
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
// +build darwin dragonfly freebsd linux,!appengine netbsd openbsd solaris
|
||||
|
||||
package readline
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type winsize struct {
|
||||
Row uint16
|
||||
Col uint16
|
||||
Xpixel uint16
|
||||
Ypixel uint16
|
||||
}
|
||||
|
||||
// SuspendMe use to send suspend signal to myself, when we in the raw mode.
|
||||
// For OSX it need to send to parent's pid
|
||||
// For Linux it need to send to myself
|
||||
func SuspendMe() {
|
||||
p, _ := os.FindProcess(os.Getppid())
|
||||
p.Signal(syscall.SIGTSTP)
|
||||
p, _ = os.FindProcess(os.Getpid())
|
||||
p.Signal(syscall.SIGTSTP)
|
||||
}
|
||||
|
||||
// get width of the terminal
|
||||
func getWidth(stdoutFd int) int {
|
||||
cols, _, err := GetSize(stdoutFd)
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
return cols
|
||||
}
|
||||
|
||||
func GetScreenWidth() int {
|
||||
w := getWidth(syscall.Stdout)
|
||||
if w < 0 {
|
||||
w = getWidth(syscall.Stderr)
|
||||
}
|
||||
return w
|
||||
}
|
||||
|
||||
// ClearScreen clears the console screen
|
||||
func ClearScreen(w io.Writer) (int, error) {
|
||||
return w.Write([]byte("\033[H"))
|
||||
}
|
||||
|
||||
func DefaultIsTerminal() bool {
|
||||
return IsTerminal(syscall.Stdin) && (IsTerminal(syscall.Stdout) || IsTerminal(syscall.Stderr))
|
||||
}
|
||||
|
||||
func GetStdin() int {
|
||||
return syscall.Stdin
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
|
||||
var (
|
||||
widthChange sync.Once
|
||||
widthChangeCallback func()
|
||||
)
|
||||
|
||||
func DefaultOnWidthChanged(f func()) {
|
||||
widthChangeCallback = f
|
||||
widthChange.Do(func() {
|
||||
ch := make(chan os.Signal, 1)
|
||||
signal.Notify(ch, syscall.SIGWINCH)
|
||||
|
||||
go func() {
|
||||
for {
|
||||
_, ok := <-ch
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
widthChangeCallback()
|
||||
}
|
||||
}()
|
||||
})
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
// +build windows
|
||||
|
||||
package readline
|
||||
|
||||
import (
|
||||
"io"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func SuspendMe() {
|
||||
}
|
||||
|
||||
func GetStdin() int {
|
||||
return int(syscall.Stdin)
|
||||
}
|
||||
|
||||
func init() {
|
||||
isWindows = true
|
||||
}
|
||||
|
||||
// get width of the terminal
|
||||
func GetScreenWidth() int {
|
||||
info, _ := GetConsoleScreenBufferInfo()
|
||||
if info == nil {
|
||||
return -1
|
||||
}
|
||||
return int(info.dwSize.x)
|
||||
}
|
||||
|
||||
// ClearScreen clears the console screen
|
||||
func ClearScreen(_ io.Writer) error {
|
||||
return SetConsoleCursorPosition(&_COORD{0, 0})
|
||||
}
|
||||
|
||||
func DefaultIsTerminal() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func DefaultOnWidthChanged(func()) {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
package readline
|
||||
|
||||
const (
|
||||
VIM_NORMAL = iota
|
||||
VIM_INSERT
|
||||
VIM_VISUAL
|
||||
)
|
||||
|
||||
type opVim struct {
|
||||
cfg *Config
|
||||
op *Operation
|
||||
vimMode int
|
||||
}
|
||||
|
||||
func newVimMode(op *Operation) *opVim {
|
||||
ov := &opVim{
|
||||
cfg: op.cfg,
|
||||
op: op,
|
||||
}
|
||||
ov.SetVimMode(ov.cfg.VimMode)
|
||||
return ov
|
||||
}
|
||||
|
||||
func (o *opVim) SetVimMode(on bool) {
|
||||
if o.cfg.VimMode && !on { // turn off
|
||||
o.ExitVimMode()
|
||||
}
|
||||
o.cfg.VimMode = on
|
||||
o.vimMode = VIM_INSERT
|
||||
}
|
||||
|
||||
func (o *opVim) ExitVimMode() {
|
||||
o.vimMode = VIM_INSERT
|
||||
}
|
||||
|
||||
func (o *opVim) IsEnableVimMode() bool {
|
||||
return o.cfg.VimMode
|
||||
}
|
||||
|
||||
func (o *opVim) handleVimNormalMovement(r rune, readNext func() rune) (t rune, handled bool) {
|
||||
rb := o.op.buf
|
||||
handled = true
|
||||
switch r {
|
||||
case 'h':
|
||||
t = CharBackward
|
||||
case 'j':
|
||||
t = CharNext
|
||||
case 'k':
|
||||
t = CharPrev
|
||||
case 'l':
|
||||
t = CharForward
|
||||
case '0', '^':
|
||||
rb.MoveToLineStart()
|
||||
case '$':
|
||||
rb.MoveToLineEnd()
|
||||
case 'x':
|
||||
rb.Delete()
|
||||
if rb.IsCursorInEnd() {
|
||||
rb.MoveBackward()
|
||||
}
|
||||
case 'r':
|
||||
rb.Replace(readNext())
|
||||
case 'd':
|
||||
next := readNext()
|
||||
switch next {
|
||||
case 'd':
|
||||
rb.Erase()
|
||||
case 'w':
|
||||
rb.DeleteWord()
|
||||
case 'h':
|
||||
rb.Backspace()
|
||||
case 'l':
|
||||
rb.Delete()
|
||||
}
|
||||
case 'p':
|
||||
rb.Yank()
|
||||
case 'b', 'B':
|
||||
rb.MoveToPrevWord()
|
||||
case 'w', 'W':
|
||||
rb.MoveToNextWord()
|
||||
case 'e', 'E':
|
||||
rb.MoveToEndWord()
|
||||
case 'f', 'F', 't', 'T':
|
||||
next := readNext()
|
||||
prevChar := r == 't' || r == 'T'
|
||||
reverse := r == 'F' || r == 'T'
|
||||
switch next {
|
||||
case CharEsc:
|
||||
default:
|
||||
rb.MoveTo(next, prevChar, reverse)
|
||||
}
|
||||
default:
|
||||
return r, false
|
||||
}
|
||||
return t, true
|
||||
}
|
||||
|
||||
func (o *opVim) handleVimNormalEnterInsert(r rune, readNext func() rune) (t rune, handled bool) {
|
||||
rb := o.op.buf
|
||||
handled = true
|
||||
switch r {
|
||||
case 'i':
|
||||
case 'I':
|
||||
rb.MoveToLineStart()
|
||||
case 'a':
|
||||
rb.MoveForward()
|
||||
case 'A':
|
||||
rb.MoveToLineEnd()
|
||||
case 's':
|
||||
rb.Delete()
|
||||
case 'S':
|
||||
rb.Erase()
|
||||
case 'c':
|
||||
next := readNext()
|
||||
switch next {
|
||||
case 'c':
|
||||
rb.Erase()
|
||||
case 'w':
|
||||
rb.DeleteWord()
|
||||
case 'h':
|
||||
rb.Backspace()
|
||||
case 'l':
|
||||
rb.Delete()
|
||||
}
|
||||
default:
|
||||
return r, false
|
||||
}
|
||||
|
||||
o.EnterVimInsertMode()
|
||||
return
|
||||
}
|
||||
|
||||
func (o *opVim) HandleVimNormal(r rune, readNext func() rune) (t rune) {
|
||||
switch r {
|
||||
case CharEnter, CharInterrupt:
|
||||
o.ExitVimMode()
|
||||
return r
|
||||
}
|
||||
|
||||
if r, handled := o.handleVimNormalMovement(r, readNext); handled {
|
||||
return r
|
||||
}
|
||||
|
||||
if r, handled := o.handleVimNormalEnterInsert(r, readNext); handled {
|
||||
return r
|
||||
}
|
||||
|
||||
// invalid operation
|
||||
o.op.t.Bell()
|
||||
return 0
|
||||
}
|
||||
|
||||
func (o *opVim) EnterVimInsertMode() {
|
||||
o.vimMode = VIM_INSERT
|
||||
}
|
||||
|
||||
func (o *opVim) ExitVimInsertMode() {
|
||||
o.vimMode = VIM_NORMAL
|
||||
}
|
||||
|
||||
func (o *opVim) HandleVim(r rune, readNext func() rune) rune {
|
||||
if o.vimMode == VIM_NORMAL {
|
||||
return o.HandleVimNormal(r, readNext)
|
||||
}
|
||||
if r == CharEsc {
|
||||
o.ExitVimInsertMode()
|
||||
return 0
|
||||
}
|
||||
|
||||
switch o.vimMode {
|
||||
case VIM_INSERT:
|
||||
return r
|
||||
case VIM_VISUAL:
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
|
@ -0,0 +1,152 @@
|
|||
// +build windows
|
||||
|
||||
package readline
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
kernel = NewKernel()
|
||||
stdout = uintptr(syscall.Stdout)
|
||||
stdin = uintptr(syscall.Stdin)
|
||||
)
|
||||
|
||||
type Kernel struct {
|
||||
SetConsoleCursorPosition,
|
||||
SetConsoleTextAttribute,
|
||||
FillConsoleOutputCharacterW,
|
||||
FillConsoleOutputAttribute,
|
||||
ReadConsoleInputW,
|
||||
GetConsoleScreenBufferInfo,
|
||||
GetConsoleCursorInfo,
|
||||
GetStdHandle CallFunc
|
||||
}
|
||||
|
||||
type short int16
|
||||
type word uint16
|
||||
type dword uint32
|
||||
type wchar uint16
|
||||
|
||||
type _COORD struct {
|
||||
x short
|
||||
y short
|
||||
}
|
||||
|
||||
func (c *_COORD) ptr() uintptr {
|
||||
return uintptr(*(*int32)(unsafe.Pointer(c)))
|
||||
}
|
||||
|
||||
const (
|
||||
EVENT_KEY = 0x0001
|
||||
EVENT_MOUSE = 0x0002
|
||||
EVENT_WINDOW_BUFFER_SIZE = 0x0004
|
||||
EVENT_MENU = 0x0008
|
||||
EVENT_FOCUS = 0x0010
|
||||
)
|
||||
|
||||
type _KEY_EVENT_RECORD struct {
|
||||
bKeyDown int32
|
||||
wRepeatCount word
|
||||
wVirtualKeyCode word
|
||||
wVirtualScanCode word
|
||||
unicodeChar wchar
|
||||
dwControlKeyState dword
|
||||
}
|
||||
|
||||
// KEY_EVENT_RECORD KeyEvent;
|
||||
// MOUSE_EVENT_RECORD MouseEvent;
|
||||
// WINDOW_BUFFER_SIZE_RECORD WindowBufferSizeEvent;
|
||||
// MENU_EVENT_RECORD MenuEvent;
|
||||
// FOCUS_EVENT_RECORD FocusEvent;
|
||||
type _INPUT_RECORD struct {
|
||||
EventType word
|
||||
Padding uint16
|
||||
Event [16]byte
|
||||
}
|
||||
|
||||
type _CONSOLE_SCREEN_BUFFER_INFO struct {
|
||||
dwSize _COORD
|
||||
dwCursorPosition _COORD
|
||||
wAttributes word
|
||||
srWindow _SMALL_RECT
|
||||
dwMaximumWindowSize _COORD
|
||||
}
|
||||
|
||||
type _SMALL_RECT struct {
|
||||
left short
|
||||
top short
|
||||
right short
|
||||
bottom short
|
||||
}
|
||||
|
||||
type _CONSOLE_CURSOR_INFO struct {
|
||||
dwSize dword
|
||||
bVisible bool
|
||||
}
|
||||
|
||||
type CallFunc func(u ...uintptr) error
|
||||
|
||||
func NewKernel() *Kernel {
|
||||
k := &Kernel{}
|
||||
kernel32 := syscall.NewLazyDLL("kernel32.dll")
|
||||
v := reflect.ValueOf(k).Elem()
|
||||
t := v.Type()
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
name := t.Field(i).Name
|
||||
f := kernel32.NewProc(name)
|
||||
v.Field(i).Set(reflect.ValueOf(k.Wrap(f)))
|
||||
}
|
||||
return k
|
||||
}
|
||||
|
||||
func (k *Kernel) Wrap(p *syscall.LazyProc) CallFunc {
|
||||
return func(args ...uintptr) error {
|
||||
var r0 uintptr
|
||||
var e1 syscall.Errno
|
||||
size := uintptr(len(args))
|
||||
if len(args) <= 3 {
|
||||
buf := make([]uintptr, 3)
|
||||
copy(buf, args)
|
||||
r0, _, e1 = syscall.Syscall(p.Addr(), size,
|
||||
buf[0], buf[1], buf[2])
|
||||
} else {
|
||||
buf := make([]uintptr, 6)
|
||||
copy(buf, args)
|
||||
r0, _, e1 = syscall.Syscall6(p.Addr(), size,
|
||||
buf[0], buf[1], buf[2], buf[3], buf[4], buf[5],
|
||||
)
|
||||
}
|
||||
|
||||
if int(r0) == 0 {
|
||||
if e1 != 0 {
|
||||
return error(e1)
|
||||
} else {
|
||||
return syscall.EINVAL
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func GetConsoleScreenBufferInfo() (*_CONSOLE_SCREEN_BUFFER_INFO, error) {
|
||||
t := new(_CONSOLE_SCREEN_BUFFER_INFO)
|
||||
err := kernel.GetConsoleScreenBufferInfo(
|
||||
stdout,
|
||||
uintptr(unsafe.Pointer(t)),
|
||||
)
|
||||
return t, err
|
||||
}
|
||||
|
||||
func GetConsoleCursorInfo() (*_CONSOLE_CURSOR_INFO, error) {
|
||||
t := new(_CONSOLE_CURSOR_INFO)
|
||||
err := kernel.GetConsoleCursorInfo(stdout, uintptr(unsafe.Pointer(t)))
|
||||
return t, err
|
||||
}
|
||||
|
||||
func SetConsoleCursorPosition(c *_COORD) error {
|
||||
return kernel.SetConsoleCursorPosition(stdout, c.ptr())
|
||||
}
|
||||
|
|
@ -34,7 +34,7 @@ RUNC_COMMIT := v1.0.0-rc8
|
|||
LIBSECCOMP_COMMIT := release-2.3
|
||||
|
||||
EXTRA_LDFLAGS ?=
|
||||
LDFLAGS := -ldflags '-X main.GitCommit=$(GIT_COMMIT) -X main.buildInfo=$(SOURCE_DATE_EPOCH) -X main.cniVersion=$(CNI_COMMIT) $(EXTRA_LDFLAGS)'
|
||||
BUILDAH_LDFLAGS := -ldflags '-X main.GitCommit=$(GIT_COMMIT) -X main.buildInfo=$(SOURCE_DATE_EPOCH) -X main.cniVersion=$(CNI_COMMIT) $(EXTRA_LDFLAGS)'
|
||||
SOURCES=*.go imagebuildah/*.go bind/*.go chroot/*.go cmd/buildah/*.go copier/*.go docker/*.go pkg/blobcache/*.go pkg/cli/*.go pkg/parse/*.go util/*.go
|
||||
|
||||
LINTFLAGS ?=
|
||||
|
|
@ -56,7 +56,7 @@ static:
|
|||
|
||||
.PHONY: bin/buildah
|
||||
bin/buildah: $(SOURCES)
|
||||
$(GO_BUILD) $(LDFLAGS) -o $@ $(BUILDFLAGS) ./cmd/buildah
|
||||
$(GO_BUILD) $(BUILDAH_LDFLAGS) -o $@ $(BUILDFLAGS) ./cmd/buildah
|
||||
|
||||
.PHONY: buildah
|
||||
buildah: bin/buildah
|
||||
|
|
@ -67,11 +67,11 @@ cross: bin/buildah.darwin.amd64 bin/buildah.linux.386 bin/buildah.linux.amd64 bi
|
|||
.PHONY: bin/buildah.%
|
||||
bin/buildah.%:
|
||||
mkdir -p ./bin
|
||||
GOOS=$(word 2,$(subst ., ,$@)) GOARCH=$(word 3,$(subst ., ,$@)) $(GO_BUILD) $(LDFLAGS) -o $@ -tags "containers_image_openpgp" ./cmd/buildah
|
||||
GOOS=$(word 2,$(subst ., ,$@)) GOARCH=$(word 3,$(subst ., ,$@)) $(GO_BUILD) $(BUILDAH_LDFLAGS) -o $@ -tags "containers_image_openpgp" ./cmd/buildah
|
||||
|
||||
.PHONY: bin/imgtype
|
||||
bin/imgtype: *.go docker/*.go util/*.go tests/imgtype/imgtype.go
|
||||
$(GO_BUILD) $(LDFLAGS) -o $@ $(BUILDFLAGS) ./tests/imgtype/imgtype.go
|
||||
$(GO_BUILD) $(BUILDAH_LDFLAGS) -o $@ $(BUILDFLAGS) ./tests/imgtype/imgtype.go
|
||||
|
||||
.PHONY: clean
|
||||
clean:
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ func sourceIsRemote(source string) bool {
|
|||
}
|
||||
|
||||
// getURL writes a tar archive containing the named content
|
||||
func getURL(src, mountpoint, renameTarget string, writer io.Writer) error {
|
||||
func getURL(src string, chown *idtools.IDPair, mountpoint, renameTarget string, writer io.Writer) error {
|
||||
url, err := url.Parse(src)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -122,10 +122,18 @@ func getURL(src, mountpoint, renameTarget string, writer io.Writer) error {
|
|||
// Write the output archive. Set permissions for compatibility.
|
||||
tw := tar.NewWriter(writer)
|
||||
defer tw.Close()
|
||||
uid := 0
|
||||
gid := 0
|
||||
if chown != nil {
|
||||
uid = chown.UID
|
||||
gid = chown.GID
|
||||
}
|
||||
hdr := tar.Header{
|
||||
Typeflag: tar.TypeReg,
|
||||
Name: name,
|
||||
Size: size,
|
||||
Uid: uid,
|
||||
Gid: gid,
|
||||
Mode: 0600,
|
||||
ModTime: date,
|
||||
}
|
||||
|
|
@ -323,7 +331,7 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption
|
|||
pipeReader, pipeWriter := io.Pipe()
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
getErr = getURL(src, mountPoint, renameTarget, pipeWriter)
|
||||
getErr = getURL(src, chownFiles, mountPoint, renameTarget, pipeWriter)
|
||||
pipeWriter.Close()
|
||||
wg.Done()
|
||||
}()
|
||||
|
|
@ -341,9 +349,9 @@ func (b *Builder) Add(destination string, extract bool, options AddAndCopyOption
|
|||
putOptions := copier.PutOptions{
|
||||
UIDMap: destUIDMap,
|
||||
GIDMap: destGIDMap,
|
||||
ChownDirs: chownDirs,
|
||||
ChownDirs: nil,
|
||||
ChmodDirs: nil,
|
||||
ChownFiles: chownFiles,
|
||||
ChownFiles: nil,
|
||||
ChmodFiles: nil,
|
||||
}
|
||||
putErr = copier.Put(mountPoint, extractDirectory, putOptions, io.TeeReader(pipeReader, hasher))
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
cc -E - > /dev/null 2> /dev/null << EOF
|
||||
${CPP:-${CC:-cc} -E} ${CPPFLAGS} - > /dev/null 2> /dev/null << EOF
|
||||
#include <btrfs/ioctl.h>
|
||||
EOF
|
||||
if test $? -ne 0 ; then
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
#!/usr/bin/env bash
|
||||
cc -E - > /dev/null 2> /dev/null << EOF
|
||||
${CPP:-${CC:-cc} -E} ${CPPFLAGS} - > /dev/null 2> /dev/null << EOF
|
||||
#include <btrfs/version.h>
|
||||
EOF
|
||||
if test $? -ne 0 ; then
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ const (
|
|||
Package = "buildah"
|
||||
// Version for the Package. Bump version in contrib/rpm/buildah.spec
|
||||
// too.
|
||||
Version = "1.17.0"
|
||||
Version = "1.18.0-dev"
|
||||
// The value we use to identify what type of information, currently a
|
||||
// serialized Builder structure, we are using as per-container state.
|
||||
// This should only be changed when we make incompatible changes to
|
||||
|
|
|
|||
|
|
@ -7,7 +7,9 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"os"
|
||||
"os/user"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -35,6 +37,14 @@ const (
|
|||
|
||||
func init() {
|
||||
reexec.Register(copierCommand, copierMain)
|
||||
// Attempt a user and host lookup to force libc (glibc, and possibly others that use dynamic
|
||||
// modules to handle looking up user and host information) to load modules that match the libc
|
||||
// our binary is currently using. Hopefully they're loaded on first use, so that they won't
|
||||
// need to be loaded after we've chrooted into the rootfs, which could include modules that
|
||||
// don't match our libc and which can't be loaded, or modules which we don't want to execute
|
||||
// because we don't trust their code.
|
||||
_, _ = user.Lookup("buildah")
|
||||
_, _ = net.LookupHost("localhost")
|
||||
}
|
||||
|
||||
// isArchivePath returns true if the specified path can be read like a (possibly
|
||||
|
|
|
|||
|
|
@ -45,6 +45,11 @@ func Lgetxattrs(path string) (map[string]string, error) {
|
|||
listSize *= 2
|
||||
continue
|
||||
}
|
||||
if (unwrapError(err) == syscall.ENOTSUP) || (unwrapError(err) == syscall.ENOSYS) {
|
||||
// treat these errors listing xattrs as equivalent to "no xattrs"
|
||||
list = list[:0]
|
||||
break
|
||||
}
|
||||
return nil, errors.Wrapf(err, "error listing extended attributes of %q", path)
|
||||
}
|
||||
list = list[:size]
|
||||
|
|
|
|||
|
|
@ -5,10 +5,10 @@ go 1.12
|
|||
require (
|
||||
github.com/containerd/containerd v1.4.1 // indirect
|
||||
github.com/containernetworking/cni v0.7.2-0.20190904153231-83439463f784
|
||||
github.com/containers/common v0.26.2
|
||||
github.com/containers/image/v5 v5.7.0
|
||||
github.com/containers/common v0.26.3
|
||||
github.com/containers/image/v5 v5.8.0
|
||||
github.com/containers/ocicrypt v1.0.3
|
||||
github.com/containers/storage v1.23.7
|
||||
github.com/containers/storage v1.23.9
|
||||
github.com/docker/distribution v2.7.1+incompatible
|
||||
github.com/docker/docker v17.12.0-ce-rc1.0.20201020191947-73dc6a680cdd+incompatible // indirect
|
||||
github.com/docker/go-units v0.4.0
|
||||
|
|
@ -17,6 +17,7 @@ require (
|
|||
github.com/ghodss/yaml v1.0.0
|
||||
github.com/hashicorp/go-multierror v1.1.0
|
||||
github.com/ishidawataru/sctp v0.0.0-20191218070446-00ab2ac2db07 // indirect
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||
github.com/mattn/go-shellwords v1.0.10
|
||||
github.com/moby/sys/mount v0.1.1 // indirect
|
||||
github.com/moby/term v0.0.0-20200915141129-7f0af18e79f2 // indirect
|
||||
|
|
|
|||
|
|
@ -45,6 +45,12 @@ github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnweb
|
|||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc=
|
||||
github.com/checkpoint-restore/go-criu/v4 v4.0.2/go.mod h1:xUQBLp4RLc5zJtWY++yjOoMoB5lihDt7fai+75m+rGw=
|
||||
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/cilium/ebpf v0.0.0-20200507155900-a9f01edf17e3/go.mod h1:XT+cAw5wfvsodedcijoh1l9cf7v1x9FlFB/3VmF/O8s=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
|
|
@ -65,17 +71,20 @@ github.com/containerd/ttrpc v0.0.0-20190828154514-0e0f228740de/go.mod h1:PvCDdDG
|
|||
github.com/containerd/typeurl v0.0.0-20180627222232-a93fcdb778cd/go.mod h1:Cm3kwCdlkCfMSHURc+r6fwoGH6/F1hH3S4sg0rLFWPc=
|
||||
github.com/containernetworking/cni v0.7.2-0.20190904153231-83439463f784 h1:rqUVLD8I859xRgUx/WMC3v7QAFqbLKZbs+0kqYboRJc=
|
||||
github.com/containernetworking/cni v0.7.2-0.20190904153231-83439463f784/go.mod h1:LGwApLUm2FpoOfxTDEeq8T9ipbpZ61X79hmU3w8FmsY=
|
||||
github.com/containers/common v0.26.2 h1:TysMCBpzq3gDFD9GzM0TKTGjtq/9HySWevKtlrvVGRU=
|
||||
github.com/containers/common v0.26.2/go.mod h1:igUeog5hx8rYhJk67rG6rGAh3zEcf0Uxuzm9KpXzo2E=
|
||||
github.com/containers/common v0.26.3 h1:5Kb5fMmJ7/xMiJ+iEbPA+5pQpl/FGxCgJex4nml4Slo=
|
||||
github.com/containers/common v0.26.3/go.mod h1:hJWZIlrl5MsE2ELNRa+MPp6I1kPbXHauuj0Ym4BsLG4=
|
||||
github.com/containers/image/v5 v5.7.0 h1:fiTC8/Xbr+zEP6njGTZtPW/3UD7MC93nC9DbUoWdxkA=
|
||||
github.com/containers/image/v5 v5.7.0/go.mod h1:8aOy+YaItukxghRORkvhq5ibWttHErzDLy6egrKfKos=
|
||||
github.com/containers/image/v5 v5.8.0 h1:B3FGHi0bdGXgg698kBIGOlHCXN5n+scJr6/5354GOPU=
|
||||
github.com/containers/image/v5 v5.8.0/go.mod h1:jKxdRtyIDumVa56hdsZvV+gwx4zB50hRou6pIuCWLkg=
|
||||
github.com/containers/libtrust v0.0.0-20190913040956-14b96171aa3b h1:Q8ePgVfHDplZ7U33NwHZkrVELsZP5fYj9pM5WBZB2GE=
|
||||
github.com/containers/libtrust v0.0.0-20190913040956-14b96171aa3b/go.mod h1:9rfv8iPl1ZP7aqh9YA68wnZv2NUDbXdcdPHVz0pFbPY=
|
||||
github.com/containers/ocicrypt v1.0.3 h1:vYgl+RZ9Q3DPMuTfxmN+qp0X2Bj52uuY2vnt6GzVe1c=
|
||||
github.com/containers/ocicrypt v1.0.3/go.mod h1:CUBa+8MRNL/VkpxYIpaMtgn1WgXGyvPQj8jcy0EVG6g=
|
||||
github.com/containers/storage v1.23.6/go.mod h1:haFs0HRowKwyzvWEx9EgI3WsL8XCSnBDb5f8P5CAxJY=
|
||||
github.com/containers/storage v1.23.7 h1:43ImvG/npvQSZXRjaudVvKISIuZSfI6qvtSNQQSGO/A=
|
||||
github.com/containers/storage v1.23.7/go.mod h1:cUT2zHjtx+WlVri30obWmM2gpqpi8jfPsmIzP1TVpEI=
|
||||
github.com/containers/storage v1.23.9 h1:qbgnTp76pLSyW3vYwY5GH4vk5cHYVXFJ+CsUEBp9TMw=
|
||||
github.com/containers/storage v1.23.9/go.mod h1:3b2ktpB6pw53SEeIoFfO0sQfP9+IoJJKPq5iJk74gxE=
|
||||
github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk=
|
||||
github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
|
|
@ -214,25 +223,37 @@ github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCV
|
|||
github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
||||
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=
|
||||
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
|
||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/errcheck v1.2.0/go.mod h1:/BMXB+zMLi60iA8Vv6Ksmxu/1UDYcXs4uQLJ+jE2L00=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/klauspost/compress v1.11.1 h1:bPb7nMRdOZYDrpPMTA3EInUQrdgoBinqUuSwlGdKDdE=
|
||||
github.com/klauspost/compress v1.11.1/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/compress v1.11.2 h1:MiK62aErc3gIiVEtyzKfeOHgW7atJb5g/KNX5m3c2nQ=
|
||||
github.com/klauspost/compress v1.11.2/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
|
||||
github.com/klauspost/pgzip v1.2.5 h1:qnWYvvKqedOF2ulHpMG72XQol4ILEJ8k2wwRl/Km8oE=
|
||||
github.com/klauspost/pgzip v1.2.5/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw=
|
||||
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/magiconair/properties v1.8.1/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo=
|
||||
github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ=
|
||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0=
|
||||
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||
github.com/mattn/go-shellwords v1.0.10 h1:Y7Xqm8piKOO3v10Thp7Z36h4FYFjt5xB//6XvOrs2Gw=
|
||||
|
|
@ -479,6 +500,7 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h
|
|||
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
|
|
|
|||
|
|
@ -216,20 +216,19 @@ func BuildDockerfiles(ctx context.Context, store storage.Store, options BuildOpt
|
|||
}
|
||||
data = resp.Body
|
||||
} else {
|
||||
// If the Dockerfile isn't found try prepending the
|
||||
// context directory to it.
|
||||
dinfo, err := os.Stat(dfile)
|
||||
if os.IsNotExist(err) {
|
||||
// If they are "/workDir/Dockerfile" and "/workDir"
|
||||
// so don't joint it
|
||||
if err != nil {
|
||||
// If the Dockerfile isn't available, try again with
|
||||
// context directory prepended (if not prepended yet).
|
||||
if !strings.HasPrefix(dfile, options.ContextDirectory) {
|
||||
dfile = filepath.Join(options.ContextDirectory, dfile)
|
||||
}
|
||||
dinfo, err = os.Stat(dfile)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
dinfo, err = os.Stat(dfile)
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
// If given a directory, add '/Dockerfile' to it.
|
||||
if dinfo.Mode().IsDir() {
|
||||
dfile = filepath.Join(dfile, "Dockerfile")
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import (
|
|||
"github.com/containers/buildah/util"
|
||||
"github.com/containers/common/pkg/config"
|
||||
"github.com/containers/image/v5/docker/reference"
|
||||
"github.com/containers/image/v5/manifest"
|
||||
is "github.com/containers/image/v5/storage"
|
||||
"github.com/containers/image/v5/transports"
|
||||
"github.com/containers/image/v5/transports/alltransports"
|
||||
|
|
@ -111,6 +112,15 @@ type Executor struct {
|
|||
stagesSemaphore *semaphore.Weighted
|
||||
jobs int
|
||||
logRusage bool
|
||||
imageInfoLock sync.Mutex
|
||||
imageInfoCache map[string]imageTypeAndHistoryAndDiffIDs
|
||||
}
|
||||
|
||||
type imageTypeAndHistoryAndDiffIDs struct {
|
||||
manifestType string
|
||||
history []v1.History
|
||||
diffIDs []digest.Digest
|
||||
err error
|
||||
}
|
||||
|
||||
// NewExecutor creates a new instance of the imagebuilder.Executor interface.
|
||||
|
|
@ -215,6 +225,7 @@ func NewExecutor(store storage.Store, options BuildOptions, mainNode *parser.Nod
|
|||
terminatedStage: make(map[string]struct{}),
|
||||
jobs: jobs,
|
||||
logRusage: options.LogRusage,
|
||||
imageInfoCache: make(map[string]imageTypeAndHistoryAndDiffIDs),
|
||||
}
|
||||
if exec.err == nil {
|
||||
exec.err = os.Stderr
|
||||
|
|
@ -335,22 +346,43 @@ func (b *Executor) waitForStage(ctx context.Context, name string, stages imagebu
|
|||
}
|
||||
}
|
||||
|
||||
// getImageHistoryAndDiffIDs returns the history and diff IDs list of imageID.
|
||||
func (b *Executor) getImageHistoryAndDiffIDs(ctx context.Context, imageID string) ([]v1.History, []digest.Digest, error) {
|
||||
// getImageTypeAndHistoryAndDiffIDs returns the manifest type, history, and diff IDs list of imageID.
|
||||
func (b *Executor) getImageTypeAndHistoryAndDiffIDs(ctx context.Context, imageID string) (string, []v1.History, []digest.Digest, error) {
|
||||
b.imageInfoLock.Lock()
|
||||
imageInfo, ok := b.imageInfoCache[imageID]
|
||||
b.imageInfoLock.Unlock()
|
||||
if ok {
|
||||
return imageInfo.manifestType, imageInfo.history, imageInfo.diffIDs, imageInfo.err
|
||||
}
|
||||
imageRef, err := is.Transport.ParseStoreReference(b.store, "@"+imageID)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "error getting image reference %q", imageID)
|
||||
return "", nil, nil, errors.Wrapf(err, "error getting image reference %q", imageID)
|
||||
}
|
||||
ref, err := imageRef.NewImage(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "error creating new image from reference to image %q", imageID)
|
||||
return "", nil, nil, errors.Wrapf(err, "error creating new image from reference to image %q", imageID)
|
||||
}
|
||||
defer ref.Close()
|
||||
oci, err := ref.OCIConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "error getting possibly-converted OCI config of image %q", imageID)
|
||||
return "", nil, nil, errors.Wrapf(err, "error getting possibly-converted OCI config of image %q", imageID)
|
||||
}
|
||||
return oci.History, oci.RootFS.DiffIDs, nil
|
||||
manifestBytes, manifestFormat, err := ref.Manifest(ctx)
|
||||
if err != nil {
|
||||
return "", nil, nil, errors.Wrapf(err, "error getting manifest of image %q", imageID)
|
||||
}
|
||||
if manifestFormat == "" && len(manifestBytes) > 0 {
|
||||
manifestFormat = manifest.GuessMIMEType(manifestBytes)
|
||||
}
|
||||
b.imageInfoLock.Lock()
|
||||
b.imageInfoCache[imageID] = imageTypeAndHistoryAndDiffIDs{
|
||||
manifestType: manifestFormat,
|
||||
history: oci.History,
|
||||
diffIDs: oci.RootFS.DiffIDs,
|
||||
err: nil,
|
||||
}
|
||||
b.imageInfoLock.Unlock()
|
||||
return manifestFormat, oci.History, oci.RootFS.DiffIDs, nil
|
||||
}
|
||||
|
||||
func (b *Executor) buildStage(ctx context.Context, cleanupStages map[int]*StageExecutor, stages imagebuilder.Stages, stageIndex int) (imageID string, ref reference.Canonical, err error) {
|
||||
|
|
|
|||
|
|
@ -1110,7 +1110,7 @@ func (s *StageExecutor) intermediateImageExists(ctx context.Context, currNode *p
|
|||
var baseHistory []v1.History
|
||||
var baseDiffIDs []digest.Digest
|
||||
if s.builder.FromImageID != "" {
|
||||
baseHistory, baseDiffIDs, err = s.executor.getImageHistoryAndDiffIDs(ctx, s.builder.FromImageID)
|
||||
_, baseHistory, baseDiffIDs, err = s.executor.getImageTypeAndHistoryAndDiffIDs(ctx, s.builder.FromImageID)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "error getting history of base image %q", s.builder.FromImageID)
|
||||
}
|
||||
|
|
@ -1142,10 +1142,15 @@ func (s *StageExecutor) intermediateImageExists(ctx context.Context, currNode *p
|
|||
}
|
||||
// Next we double check that the history of this image is equivalent to the previous
|
||||
// lines in the Dockerfile up till the point we are at in the build.
|
||||
history, diffIDs, err := s.executor.getImageHistoryAndDiffIDs(ctx, image.ID)
|
||||
manifestType, history, diffIDs, err := s.executor.getImageTypeAndHistoryAndDiffIDs(ctx, image.ID)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "error getting history of %q", image.ID)
|
||||
}
|
||||
// If this candidate isn't of the type that we're building, then it may have lost
|
||||
// some format-specific information that a building-without-cache run wouldn't lose.
|
||||
if manifestType != s.executor.outputFormat {
|
||||
continue
|
||||
}
|
||||
// children + currNode is the point of the Dockerfile we are currently at.
|
||||
if s.historyAndDiffIDsMatch(baseHistory, baseDiffIDs, currNode, history, diffIDs, addedContentDigest, buildAddsLayer) {
|
||||
return image.ID, nil
|
||||
|
|
@ -1276,5 +1281,5 @@ func (s *StageExecutor) commit(ctx context.Context, createdBy string, emptyLayer
|
|||
}
|
||||
|
||||
func (s *StageExecutor) EnsureContainerPath(path string) error {
|
||||
return copier.Mkdir(s.mountPoint, path, copier.MkdirOptions{})
|
||||
return copier.Mkdir(s.mountPoint, filepath.Join(s.mountPoint, path), copier.MkdirOptions{})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@
|
|||
tmpdir="$PWD/tmp.$RANDOM"
|
||||
mkdir -p "$tmpdir"
|
||||
trap 'rm -fr "$tmpdir"' EXIT
|
||||
cc -o "$tmpdir"/libdm_tag -ldevmapper -x c - > /dev/null 2> /dev/null << EOF
|
||||
${CC:-cc} ${CFLAGS} ${CPPFLAGS} ${LDFLAGS} -o "$tmpdir"/libdm_tag -x c - -ldevmapper > /dev/null 2> /dev/null << EOF
|
||||
#include <libdevmapper.h>
|
||||
int main() {
|
||||
struct dm_task *task;
|
||||
|
|
|
|||
|
|
@ -4,13 +4,14 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/containers/buildah/util"
|
||||
"github.com/containers/image/v5/docker"
|
||||
"github.com/containers/image/v5/image"
|
||||
"github.com/containers/image/v5/manifest"
|
||||
"github.com/containers/image/v5/pkg/sysregistriesv2"
|
||||
"github.com/containers/image/v5/pkg/shortnames"
|
||||
is "github.com/containers/image/v5/storage"
|
||||
"github.com/containers/image/v5/transports"
|
||||
"github.com/containers/image/v5/transports/alltransports"
|
||||
|
|
@ -103,145 +104,168 @@ func newContainerIDMappingOptions(idmapOptions *IDMappingOptions) storage.IDMapp
|
|||
return options
|
||||
}
|
||||
|
||||
func resolveImage(ctx context.Context, systemContext *types.SystemContext, store storage.Store, options BuilderOptions) (types.ImageReference, string, *storage.Image, error) {
|
||||
type failure struct {
|
||||
resolvedImageName string
|
||||
err error
|
||||
}
|
||||
candidates, transport, searchRegistriesWereUsedButEmpty, err := util.ResolveName(options.FromImage, options.Registry, systemContext, store)
|
||||
func resolveLocalImage(systemContext *types.SystemContext, store storage.Store, options BuilderOptions) (types.ImageReference, string, *storage.Image, error) {
|
||||
candidates, _, _, err := util.ResolveName(options.FromImage, options.Registry, systemContext, store)
|
||||
if err != nil {
|
||||
return nil, "", nil, errors.Wrapf(err, "error parsing reference to image %q", options.FromImage)
|
||||
return nil, "", nil, errors.Wrapf(err, "error resolving local image %q", options.FromImage)
|
||||
}
|
||||
|
||||
failures := []failure{}
|
||||
for _, image := range candidates {
|
||||
if transport == "" {
|
||||
img, err := store.Image(image)
|
||||
if err != nil {
|
||||
logrus.Debugf("error looking up known-local image %q: %v", image, err)
|
||||
failures = append(failures, failure{resolvedImageName: image, err: err})
|
||||
img, err := store.Image(image)
|
||||
if err != nil {
|
||||
if errors.Cause(err) == storage.ErrImageUnknown {
|
||||
continue
|
||||
}
|
||||
ref, err := is.Transport.ParseStoreReference(store, img.ID)
|
||||
if err != nil {
|
||||
return nil, "", nil, errors.Wrapf(err, "error parsing reference to image %q", img.ID)
|
||||
}
|
||||
return ref, transport, img, nil
|
||||
return nil, "", nil, err
|
||||
}
|
||||
|
||||
trans := transport
|
||||
if transport != util.DefaultTransport {
|
||||
trans = trans + ":"
|
||||
}
|
||||
srcRef, err := alltransports.ParseImageName(trans + image)
|
||||
ref, err := is.Transport.ParseStoreReference(store, img.ID)
|
||||
if err != nil {
|
||||
logrus.Debugf("error parsing image name %q: %v", trans+image, err)
|
||||
failures = append(failures, failure{
|
||||
resolvedImageName: image,
|
||||
err: errors.Wrapf(err, "error parsing attempted image name %q", trans+image),
|
||||
})
|
||||
continue
|
||||
return nil, "", nil, errors.Wrapf(err, "error parsing reference to image %q", img.ID)
|
||||
}
|
||||
return ref, ref.Transport().Name(), img, nil
|
||||
}
|
||||
|
||||
if options.PullPolicy == PullAlways {
|
||||
pulledImg, pulledReference, err := pullAndFindImage(ctx, store, srcRef, options, systemContext)
|
||||
if err != nil {
|
||||
logrus.Debugf("unable to pull and read image %q: %v", image, err)
|
||||
failures = append(failures, failure{resolvedImageName: image, err: err})
|
||||
continue
|
||||
}
|
||||
return pulledReference, transport, pulledImg, nil
|
||||
}
|
||||
return nil, "", nil, nil
|
||||
}
|
||||
|
||||
destImage, err := localImageNameForReference(ctx, store, srcRef)
|
||||
if err != nil {
|
||||
return nil, "", nil, errors.Wrapf(err, "error computing local image name for %q", transports.ImageName(srcRef))
|
||||
}
|
||||
if destImage == "" {
|
||||
return nil, "", nil, errors.Errorf("error computing local image name for %q", transports.ImageName(srcRef))
|
||||
}
|
||||
ref, err := is.Transport.ParseStoreReference(store, destImage)
|
||||
if err != nil {
|
||||
return nil, "", nil, errors.Wrapf(err, "error parsing reference to image %q", destImage)
|
||||
}
|
||||
// getShortNameMode looks up the `CONTAINERS_SHORT_NAME_ALIASING` environment
|
||||
// variable. If it's "on", return `nil` to use the defaults from
|
||||
// containers/image and the registries.conf files on the system. If it's
|
||||
// "off", empty or unset, return types.ShortNameModeDisabled to turn off
|
||||
// short-name aliasing by default.
|
||||
//
|
||||
// TODO: remove this function once we want to default to short-name aliasing.
|
||||
func getShortNameMode() *types.ShortNameMode {
|
||||
env := os.Getenv("CONTAINERS_SHORT_NAME_ALIASING")
|
||||
if strings.ToLower(env) == "on" {
|
||||
return nil // default to whatever registries.conf and c/image decide
|
||||
}
|
||||
mode := types.ShortNameModeDisabled
|
||||
return &mode
|
||||
}
|
||||
|
||||
if options.PullPolicy == PullIfNewer {
|
||||
img, err := is.Transport.GetStoreImage(store, ref)
|
||||
if err == nil {
|
||||
// Let's see if this image is on the repository and if it's there
|
||||
// then note it's Created date.
|
||||
var repoImageCreated time.Time
|
||||
repoImageFound := false
|
||||
repoImage, err := srcRef.NewImage(ctx, systemContext)
|
||||
if err == nil {
|
||||
inspect, err := repoImage.Inspect(ctx)
|
||||
if err == nil {
|
||||
repoImageFound = true
|
||||
repoImageCreated = *inspect.Created
|
||||
}
|
||||
repoImage.Close()
|
||||
}
|
||||
if !repoImageFound || repoImageCreated == img.Created {
|
||||
// The image is only local or the same date is on the
|
||||
// local and repo versions of the image, no need to pull.
|
||||
return ref, transport, img, nil
|
||||
}
|
||||
}
|
||||
func resolveImage(ctx context.Context, systemContext *types.SystemContext, store storage.Store, options BuilderOptions) (types.ImageReference, string, *storage.Image, error) {
|
||||
if systemContext == nil {
|
||||
systemContext = &types.SystemContext{}
|
||||
}
|
||||
systemContext.ShortNameMode = getShortNameMode()
|
||||
|
||||
fromImage := options.FromImage
|
||||
// If the image name includes a transport we can use it as it. Special
|
||||
// treatment for docker references which are subject to pull policies
|
||||
// that we're handling below.
|
||||
srcRef, err := alltransports.ParseImageName(options.FromImage)
|
||||
if err == nil {
|
||||
if srcRef.Transport().Name() == docker.Transport.Name() {
|
||||
fromImage = srcRef.DockerReference().String()
|
||||
} else {
|
||||
// Get the image from the store if present for PullNever and PullIfMissing
|
||||
img, err := is.Transport.GetStoreImage(store, ref)
|
||||
if err == nil {
|
||||
return ref, transport, img, nil
|
||||
pulledImg, pulledReference, err := pullAndFindImage(ctx, store, srcRef, options, systemContext)
|
||||
return pulledReference, srcRef.Transport().Name(), pulledImg, err
|
||||
}
|
||||
}
|
||||
|
||||
localImageRef, _, localImage, err := resolveLocalImage(systemContext, store, options)
|
||||
if err != nil {
|
||||
return nil, "", nil, err
|
||||
}
|
||||
|
||||
// If we could resolve the image locally, check if it was referenced by
|
||||
// ID. In that case, we don't need to bother any further and can
|
||||
// prevent prompting the user.
|
||||
if localImage != nil && strings.HasPrefix(localImage.ID, options.FromImage) {
|
||||
return localImageRef, localImageRef.Transport().Name(), localImage, nil
|
||||
}
|
||||
|
||||
if options.PullPolicy == PullNever || options.PullPolicy == PullIfMissing {
|
||||
if localImage != nil {
|
||||
return localImageRef, localImageRef.Transport().Name(), localImage, nil
|
||||
}
|
||||
if options.PullPolicy == PullNever {
|
||||
return nil, "", nil, errors.Errorf("pull policy is %q but %q could not be found locally", "never", options.FromImage)
|
||||
}
|
||||
}
|
||||
|
||||
resolved, err := shortnames.Resolve(systemContext, fromImage)
|
||||
if err != nil {
|
||||
return nil, "", nil, err
|
||||
}
|
||||
|
||||
// Print the image-resolution description unless we're looking for a
|
||||
// new image and already found a local image. In many cases, the
|
||||
// description will be more confusing than helpful (e.g., `buildah from
|
||||
// localImage`).
|
||||
if desc := resolved.Description(); len(desc) > 0 {
|
||||
logrus.Debug(desc)
|
||||
if !(options.PullPolicy == PullIfNewer && localImage != nil) {
|
||||
if options.ReportWriter != nil {
|
||||
if _, err := options.ReportWriter.Write([]byte(desc + "\n")); err != nil {
|
||||
return nil, "", nil, err
|
||||
}
|
||||
}
|
||||
if errors.Cause(err) == storage.ErrImageUnknown && options.PullPolicy == PullNever {
|
||||
logrus.Debugf("no such image %q: %v", transports.ImageName(ref), err)
|
||||
failures = append(failures, failure{
|
||||
resolvedImageName: image,
|
||||
err: errors.Errorf("no such image %q", transports.ImageName(ref)),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var pullErrors []error
|
||||
for _, pullCandidate := range resolved.PullCandidates {
|
||||
ref, err := docker.NewReference(pullCandidate.Value)
|
||||
if err != nil {
|
||||
return nil, "", nil, err
|
||||
}
|
||||
|
||||
// We're tasked to pull a "newer" image. If there's no local
|
||||
// image, we have no base for comparison, so we'll pull the
|
||||
// first available image.
|
||||
//
|
||||
// If there's a local image, the `pullCandidate` is considered
|
||||
// to be newer if its time stamp differs from the local one.
|
||||
// Otherwise, we don't pull and skip it.
|
||||
if options.PullPolicy == PullIfNewer && localImage != nil {
|
||||
remoteImage, err := ref.NewImage(ctx, systemContext)
|
||||
if err != nil {
|
||||
logrus.Debugf("unable to remote-inspect image %q: %v", pullCandidate.Value.String(), err)
|
||||
pullErrors = append(pullErrors, err)
|
||||
continue
|
||||
}
|
||||
defer remoteImage.Close()
|
||||
|
||||
remoteData, err := remoteImage.Inspect(ctx)
|
||||
if err != nil {
|
||||
logrus.Debugf("unable to remote-inspect image %q: %v", pullCandidate.Value.String(), err)
|
||||
pullErrors = append(pullErrors, err)
|
||||
continue
|
||||
}
|
||||
|
||||
// FIXME: we should compare image digests not time stamps.
|
||||
// Comparing time stamps is flawed. Be aware that fixing
|
||||
// it may entail non-trivial changes to the tests. Please
|
||||
// refer to https://github.com/containers/buildah/issues/2779
|
||||
// for more.
|
||||
if localImage.Created.Equal(*remoteData.Created) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
pulledImg, pulledReference, err := pullAndFindImage(ctx, store, srcRef, options, systemContext)
|
||||
pulledImg, pulledReference, err := pullAndFindImage(ctx, store, ref, options, systemContext)
|
||||
if err != nil {
|
||||
logrus.Debugf("unable to pull and read image %q: %v", image, err)
|
||||
failures = append(failures, failure{resolvedImageName: image, err: err})
|
||||
logrus.Debugf("unable to pull and read image %q: %v", pullCandidate.Value.String(), err)
|
||||
pullErrors = append(pullErrors, err)
|
||||
continue
|
||||
}
|
||||
return pulledReference, transport, pulledImg, nil
|
||||
|
||||
// Make sure to record the short-name alias if necessary.
|
||||
if err = pullCandidate.Record(); err != nil {
|
||||
return nil, "", nil, err
|
||||
}
|
||||
|
||||
return pulledReference, "", pulledImg, nil
|
||||
}
|
||||
|
||||
if len(failures) != len(candidates) {
|
||||
return nil, "", nil, errors.Errorf("internal error: %d candidates (%#v) vs. %d failures (%#v)", len(candidates), candidates, len(failures), failures)
|
||||
// If we were looking for a newer image but could not find one, return
|
||||
// the local image if present.
|
||||
if options.PullPolicy == PullIfNewer && localImage != nil {
|
||||
return localImageRef, localImageRef.Transport().Name(), localImage, nil
|
||||
}
|
||||
|
||||
registriesConfPath := sysregistriesv2.ConfigPath(systemContext)
|
||||
switch len(failures) {
|
||||
case 0:
|
||||
if searchRegistriesWereUsedButEmpty {
|
||||
return nil, "", nil, errors.Errorf("image name %q is a short name and no search registries are defined in %s.", options.FromImage, registriesConfPath)
|
||||
}
|
||||
return nil, "", nil, errors.Errorf("internal error: no pull candidates were available for %q for an unknown reason", options.FromImage)
|
||||
|
||||
case 1:
|
||||
err := failures[0].err
|
||||
if failures[0].resolvedImageName != options.FromImage {
|
||||
err = errors.Wrapf(err, "while pulling %q as %q", options.FromImage, failures[0].resolvedImageName)
|
||||
}
|
||||
if searchRegistriesWereUsedButEmpty {
|
||||
err = errors.Wrapf(err, "(image name %q is a short name and no search registries are defined in %s)", options.FromImage, registriesConfPath)
|
||||
}
|
||||
return nil, "", nil, err
|
||||
|
||||
default:
|
||||
// NOTE: a multi-line error string:
|
||||
e := fmt.Sprintf("The following failures happened while trying to pull image specified by %q based on search registries in %s:", options.FromImage, registriesConfPath)
|
||||
for _, f := range failures {
|
||||
e = e + fmt.Sprintf("\n* %q: %s", f.resolvedImageName, f.err.Error())
|
||||
}
|
||||
return nil, "", nil, errors.New(e)
|
||||
}
|
||||
return nil, "", nil, resolved.FormatPullErrors(pullErrors)
|
||||
}
|
||||
|
||||
func containerNameExist(name string, containers []storage.Container) bool {
|
||||
|
|
|
|||
|
|
@ -59,7 +59,6 @@ type BudResults struct {
|
|||
Creds string
|
||||
DisableCompression bool
|
||||
DisableContentTrust bool
|
||||
DecryptionKeys []string
|
||||
File []string
|
||||
Format string
|
||||
Iidfile string
|
||||
|
|
@ -90,38 +89,39 @@ type BudResults struct {
|
|||
// FromAndBugResults represents the results for common flags
|
||||
// in bud and from
|
||||
type FromAndBudResults struct {
|
||||
AddHost []string
|
||||
BlobCache string
|
||||
CapAdd []string
|
||||
CapDrop []string
|
||||
CgroupParent string
|
||||
CPUPeriod uint64
|
||||
CPUQuota int64
|
||||
CPUSetCPUs string
|
||||
CPUSetMems string
|
||||
CPUShares uint64
|
||||
Devices []string
|
||||
DNSSearch []string
|
||||
DNSServers []string
|
||||
DNSOptions []string
|
||||
HTTPProxy bool
|
||||
Isolation string
|
||||
Memory string
|
||||
MemorySwap string
|
||||
OverrideArch string
|
||||
OverrideOS string
|
||||
SecurityOpt []string
|
||||
ShmSize string
|
||||
Ulimit []string
|
||||
Volumes []string
|
||||
AddHost []string
|
||||
BlobCache string
|
||||
CapAdd []string
|
||||
CapDrop []string
|
||||
CgroupParent string
|
||||
CPUPeriod uint64
|
||||
CPUQuota int64
|
||||
CPUSetCPUs string
|
||||
CPUSetMems string
|
||||
CPUShares uint64
|
||||
DecryptionKeys []string
|
||||
Devices []string
|
||||
DNSSearch []string
|
||||
DNSServers []string
|
||||
DNSOptions []string
|
||||
HTTPProxy bool
|
||||
Isolation string
|
||||
Memory string
|
||||
MemorySwap string
|
||||
OverrideArch string
|
||||
OverrideOS string
|
||||
SecurityOpt []string
|
||||
ShmSize string
|
||||
Ulimit []string
|
||||
Volumes []string
|
||||
}
|
||||
|
||||
// GetUserNSFlags returns the common flags for usernamespace
|
||||
func GetUserNSFlags(flags *UserNSResults) pflag.FlagSet {
|
||||
usernsFlags := pflag.FlagSet{}
|
||||
usernsFlags.StringVar(&flags.UserNS, "userns", "", "'container', `path` of user namespace to join, or 'host'")
|
||||
usernsFlags.StringSliceVar(&flags.UserNSUIDMap, "userns-uid-map", []string{}, "`containerID:hostID:length` UID mapping to use in user namespace")
|
||||
usernsFlags.StringSliceVar(&flags.UserNSGIDMap, "userns-gid-map", []string{}, "`containerID:hostID:length` GID mapping to use in user namespace")
|
||||
usernsFlags.StringSliceVar(&flags.UserNSUIDMap, "userns-uid-map", []string{}, "`containerUID:hostUID:length` UID mapping to use in user namespace")
|
||||
usernsFlags.StringSliceVar(&flags.UserNSGIDMap, "userns-gid-map", []string{}, "`containerGID:hostGID:length` GID mapping to use in user namespace")
|
||||
usernsFlags.StringVar(&flags.UserNSUIDMapUser, "userns-uid-map-user", "", "`name` of entries from /etc/subuid to use to set user namespace UID mapping")
|
||||
usernsFlags.StringVar(&flags.UserNSGIDMapGroup, "userns-gid-map-group", "", "`name` of entries from /etc/subgid to use to set user namespace GID mapping")
|
||||
return usernsFlags
|
||||
|
|
@ -208,6 +208,9 @@ func GetBudFlags(flags *BudResults) pflag.FlagSet {
|
|||
fs.StringSliceVar(&flags.RuntimeFlags, "runtime-flag", []string{}, "add global flags for the container runtime")
|
||||
fs.StringVar(&flags.SignBy, "sign-by", "", "sign the image using a GPG key with the specified `FINGERPRINT`")
|
||||
fs.StringVar(&flags.SignaturePolicy, "signature-policy", "", "`pathname` of signature policy file (not usually used)")
|
||||
if err := fs.MarkHidden("signature-policy"); err != nil {
|
||||
panic(fmt.Sprintf("error marking the signature-policy flag as hidden: %v", err))
|
||||
}
|
||||
fs.BoolVar(&flags.Squash, "squash", false, "squash newly built layers into a single new layer")
|
||||
fs.StringArrayVarP(&flags.Tag, "tag", "t", []string{}, "tagged `name` to apply to the built image")
|
||||
fs.StringVar(&flags.Target, "target", "", "set the target build stage to build")
|
||||
|
|
@ -265,6 +268,7 @@ func GetFromAndBudFlags(flags *FromAndBudResults, usernsResults *UserNSResults,
|
|||
fs.Uint64VarP(&flags.CPUShares, "cpu-shares", "c", 0, "CPU shares (relative weight)")
|
||||
fs.StringVar(&flags.CPUSetCPUs, "cpuset-cpus", "", "CPUs in which to allow execution (0-3, 0,1)")
|
||||
fs.StringVar(&flags.CPUSetMems, "cpuset-mems", "", "memory nodes (MEMs) in which to allow execution (0-3, 0,1). Only effective on NUMA systems.")
|
||||
fs.StringSliceVar(&flags.DecryptionKeys, "decryption-key", nil, "key needed to decrypt the image")
|
||||
fs.StringArrayVar(&flags.Devices, "device", defaultContainerConfig.Containers.Devices, "Additional devices to be used within containers (default [])")
|
||||
fs.StringSliceVar(&flags.DNSSearch, "dns-search", defaultContainerConfig.Containers.DNSSearches, "Set custom DNS search domains")
|
||||
fs.StringSliceVar(&flags.DNSServers, "dns", defaultContainerConfig.Containers.DNSServers, "Set custom DNS servers or disable it completely by setting it to 'none', which prevents the automatic creation of `/etc/resolv.conf`.")
|
||||
|
|
@ -308,6 +312,7 @@ func GetFromAndBudFlagsCompletions() commonComp.FlagCompletions {
|
|||
flagCompletion["cpu-shares"] = commonComp.AutocompleteNone
|
||||
flagCompletion["cpuset-cpus"] = commonComp.AutocompleteNone
|
||||
flagCompletion["cpuset-mems"] = commonComp.AutocompleteNone
|
||||
flagCompletion["decryption-key"] = commonComp.AutocompleteNone
|
||||
flagCompletion["device"] = commonComp.AutocompleteDefault
|
||||
flagCompletion["dns-search"] = commonComp.AutocompleteNone
|
||||
flagCompletion["dns"] = commonComp.AutocompleteNone
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ type secretData struct {
|
|||
// saveTo saves secret data to given directory
|
||||
func (s secretData) saveTo(dir string) error {
|
||||
path := filepath.Join(dir, s.name)
|
||||
if err := os.MkdirAll(filepath.Dir(path), s.dirMode); err != nil && !os.IsExist(err) {
|
||||
if err := os.MkdirAll(filepath.Dir(path), s.dirMode); err != nil {
|
||||
return err
|
||||
}
|
||||
return ioutil.WriteFile(path, s.data, s.mode)
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/containers/buildah/pkg/blobcache"
|
||||
"github.com/containers/buildah/util"
|
||||
"github.com/containers/image/v5/directory"
|
||||
"github.com/containers/image/v5/docker"
|
||||
dockerarchive "github.com/containers/image/v5/docker/archive"
|
||||
|
|
@ -18,6 +17,7 @@ import (
|
|||
"github.com/containers/image/v5/signature"
|
||||
is "github.com/containers/image/v5/storage"
|
||||
"github.com/containers/image/v5/transports"
|
||||
"github.com/containers/image/v5/transports/alltransports"
|
||||
"github.com/containers/image/v5/types"
|
||||
encconfig "github.com/containers/ocicrypt/config"
|
||||
"github.com/containers/storage"
|
||||
|
|
@ -171,63 +171,63 @@ func Pull(ctx context.Context, imageName string, options PullOptions) (imageID s
|
|||
OciDecryptConfig: options.OciDecryptConfig,
|
||||
}
|
||||
|
||||
storageRef, transport, img, err := resolveImage(ctx, systemContext, options.Store, boptions)
|
||||
if !options.AllTags {
|
||||
_, _, img, err := resolveImage(ctx, systemContext, options.Store, boptions)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return img.ID, nil
|
||||
}
|
||||
|
||||
srcRef, err := alltransports.ParseImageName(imageName)
|
||||
if err == nil && srcRef.Transport().Name() != docker.Transport.Name() {
|
||||
return "", errors.New("Non-docker transport is not supported, for --all-tags pulling")
|
||||
}
|
||||
|
||||
storageRef, _, _, err := resolveImage(ctx, systemContext, options.Store, boptions)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var errs *multierror.Error
|
||||
if options.AllTags {
|
||||
if transport != util.DefaultTransport {
|
||||
return "", errors.New("Non-docker transport is not supported, for --all-tags pulling")
|
||||
}
|
||||
|
||||
repo := reference.TrimNamed(storageRef.DockerReference())
|
||||
dockerRef, err := docker.NewReference(reference.TagNameOnly(storageRef.DockerReference()))
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "internal error creating docker.Transport reference for %s", storageRef.DockerReference().String())
|
||||
}
|
||||
tags, err := docker.GetRepositoryTags(ctx, systemContext, dockerRef)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "error getting repository tags")
|
||||
}
|
||||
for _, tag := range tags {
|
||||
tagged, err := reference.WithTag(repo, tag)
|
||||
if err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
continue
|
||||
}
|
||||
taggedRef, err := docker.NewReference(tagged)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "internal error creating docker.Transport reference for %s", tagged.String())
|
||||
}
|
||||
if options.ReportWriter != nil {
|
||||
if _, err := options.ReportWriter.Write([]byte("Pulling " + tagged.String() + "\n")); err != nil {
|
||||
return "", errors.Wrapf(err, "error writing pull report")
|
||||
}
|
||||
}
|
||||
ref, err := pullImage(ctx, options.Store, taggedRef, options, systemContext)
|
||||
if err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
continue
|
||||
}
|
||||
taggedImg, err := is.Transport.GetStoreImage(options.Store, ref)
|
||||
if err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
continue
|
||||
}
|
||||
imageID = taggedImg.ID
|
||||
}
|
||||
} else {
|
||||
imageID = img.ID
|
||||
repo := reference.TrimNamed(storageRef.DockerReference())
|
||||
dockerRef, err := docker.NewReference(reference.TagNameOnly(storageRef.DockerReference()))
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "internal error creating docker.Transport reference for %s", storageRef.DockerReference().String())
|
||||
}
|
||||
if errs == nil {
|
||||
err = nil
|
||||
} else {
|
||||
err = errs.ErrorOrNil()
|
||||
tags, err := docker.GetRepositoryTags(ctx, systemContext, dockerRef)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "error getting repository tags")
|
||||
}
|
||||
for _, tag := range tags {
|
||||
tagged, err := reference.WithTag(repo, tag)
|
||||
if err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
continue
|
||||
}
|
||||
taggedRef, err := docker.NewReference(tagged)
|
||||
if err != nil {
|
||||
return "", errors.Wrapf(err, "internal error creating docker.Transport reference for %s", tagged.String())
|
||||
}
|
||||
if options.ReportWriter != nil {
|
||||
if _, err := options.ReportWriter.Write([]byte("Pulling " + tagged.String() + "\n")); err != nil {
|
||||
return "", errors.Wrapf(err, "error writing pull report")
|
||||
}
|
||||
}
|
||||
ref, err := pullImage(ctx, options.Store, taggedRef, options, systemContext)
|
||||
if err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
continue
|
||||
}
|
||||
taggedImg, err := is.Transport.GetStoreImage(options.Store, ref)
|
||||
if err != nil {
|
||||
errs = multierror.Append(errs, err)
|
||||
continue
|
||||
}
|
||||
imageID = taggedImg.ID
|
||||
}
|
||||
|
||||
return imageID, err
|
||||
return imageID, errs.ErrorOrNil()
|
||||
}
|
||||
|
||||
func pullImage(ctx context.Context, store storage.Store, srcRef types.ImageReference, options PullOptions, sc *types.SystemContext) (types.ImageReference, error) {
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ import (
|
|||
"github.com/containernetworking/cni/libcni"
|
||||
"github.com/containers/buildah/bind"
|
||||
"github.com/containers/buildah/chroot"
|
||||
"github.com/containers/buildah/copier"
|
||||
"github.com/containers/buildah/pkg/overlay"
|
||||
"github.com/containers/buildah/pkg/secrets"
|
||||
"github.com/containers/buildah/util"
|
||||
|
|
@ -165,11 +166,6 @@ func (b *Builder) Run(command []string, options RunOptions) error {
|
|||
spec := g.Config
|
||||
g = nil
|
||||
|
||||
logrus.Debugf("ensuring working directory %q exists", filepath.Join(mountPoint, spec.Process.Cwd))
|
||||
if err = os.MkdirAll(filepath.Join(mountPoint, spec.Process.Cwd), 0755); err != nil && !os.IsExist(err) {
|
||||
return err
|
||||
}
|
||||
|
||||
// Set the seccomp configuration using the specified profile name. Some syscalls are
|
||||
// allowed if certain capabilities are to be granted (example: CAP_SYS_CHROOT and chroot),
|
||||
// so we sorted out the capabilities lists first.
|
||||
|
|
@ -184,6 +180,15 @@ func (b *Builder) Run(command []string, options RunOptions) error {
|
|||
}
|
||||
rootIDPair := &idtools.IDPair{UID: int(rootUID), GID: int(rootGID)}
|
||||
|
||||
mode := os.FileMode(0755)
|
||||
coptions := copier.MkdirOptions{
|
||||
ChownNew: rootIDPair,
|
||||
ChmodNew: &mode,
|
||||
}
|
||||
if err := copier.Mkdir(mountPoint, filepath.Join(mountPoint, spec.Process.Cwd), coptions); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
bindFiles := make(map[string]string)
|
||||
namespaceOptions := append(b.NamespaceOptions, options.NamespaceOptions...)
|
||||
volumes := b.Volumes()
|
||||
|
|
@ -1981,7 +1986,6 @@ func (b *Builder) configureEnvironment(g *generate.Generator, options RunOptions
|
|||
}
|
||||
|
||||
func setupRootlessSpecChanges(spec *specs.Spec, bundleDir string, shmSize string) error {
|
||||
spec.Hostname = ""
|
||||
spec.Process.User.AdditionalGids = nil
|
||||
spec.Linux.Resources = nil
|
||||
|
||||
|
|
@ -2137,10 +2141,6 @@ func checkAndOverrideIsolationOptions(isolation Isolation, options *RunOptions)
|
|||
logrus.Debugf("Forcing use of a user namespace.")
|
||||
}
|
||||
options.NamespaceOptions.AddOrReplace(NamespaceOption{Name: string(specs.UserNamespace)})
|
||||
if ns := options.NamespaceOptions.Find(string(specs.UTSNamespace)); ns != nil && !ns.Host {
|
||||
logrus.Debugf("Disabling UTS namespace.")
|
||||
}
|
||||
options.NamespaceOptions.AddOrReplace(NamespaceOption{Name: string(specs.UTSNamespace), Host: true})
|
||||
case IsolationOCI:
|
||||
pidns := options.NamespaceOptions.Find(string(specs.PIDNamespace))
|
||||
userns := options.NamespaceOptions.Find(string(specs.UserNamespace))
|
||||
|
|
|
|||
|
|
@ -12,10 +12,10 @@ import (
|
|||
|
||||
"github.com/containers/common/pkg/config"
|
||||
"github.com/containers/image/v5/docker/reference"
|
||||
"github.com/containers/image/v5/pkg/shortnames"
|
||||
"github.com/containers/image/v5/pkg/sysregistriesv2"
|
||||
"github.com/containers/image/v5/signature"
|
||||
is "github.com/containers/image/v5/storage"
|
||||
"github.com/containers/image/v5/transports"
|
||||
"github.com/containers/image/v5/transports/alltransports"
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/containers/storage"
|
||||
|
|
@ -69,42 +69,10 @@ func ResolveName(name string, firstRegistry string, sc *types.SystemContext, sto
|
|||
}
|
||||
}
|
||||
|
||||
// If the image includes a transport's name as a prefix, use it as-is.
|
||||
if strings.HasPrefix(name, DefaultTransport) {
|
||||
return []string{strings.TrimPrefix(name, DefaultTransport)}, DefaultTransport, false, nil
|
||||
}
|
||||
split := strings.SplitN(name, ":", 2)
|
||||
if StartsWithValidTransport(name) && len(split) == 2 {
|
||||
if trans := transports.Get(split[0]); trans != nil {
|
||||
return []string{split[1]}, trans.Name(), false, nil
|
||||
}
|
||||
}
|
||||
// If the image name already included a domain component, we're done.
|
||||
named, err := reference.ParseNormalizedNamed(name)
|
||||
if err != nil {
|
||||
return nil, "", false, errors.Wrapf(err, "error parsing image name %q", name)
|
||||
}
|
||||
if named.String() == name {
|
||||
// Parsing produced the same result, so there was a domain name in there to begin with.
|
||||
return []string{name}, DefaultTransport, false, nil
|
||||
}
|
||||
if reference.Domain(named) != "" && RegistryDefaultPathPrefix[reference.Domain(named)] != "" {
|
||||
// If this domain can cause us to insert something in the middle, check if that happened.
|
||||
repoPath := reference.Path(named)
|
||||
domain := reference.Domain(named)
|
||||
tag := ""
|
||||
if tagged, ok := named.(reference.Tagged); ok {
|
||||
tag = ":" + tagged.Tag()
|
||||
}
|
||||
digest := ""
|
||||
if digested, ok := named.(reference.Digested); ok {
|
||||
digest = "@" + digested.Digest().String()
|
||||
}
|
||||
defaultPrefix := RegistryDefaultPathPrefix[reference.Domain(named)] + "/"
|
||||
if strings.HasPrefix(repoPath, defaultPrefix) && path.Join(domain, repoPath[len(defaultPrefix):])+tag+digest == name {
|
||||
// Yup, parsing just inserted a bit in the middle, so there was a domain name there to begin with.
|
||||
return []string{name}, DefaultTransport, false, nil
|
||||
}
|
||||
// Transports are not supported for local image look ups.
|
||||
srcRef, err := alltransports.ParseImageName(name)
|
||||
if err == nil {
|
||||
return []string{srcRef.StringWithinTransport()}, srcRef.Transport().Name(), false, nil
|
||||
}
|
||||
|
||||
// Figure out the list of registries.
|
||||
|
|
@ -126,25 +94,26 @@ func ResolveName(name string, firstRegistry string, sc *types.SystemContext, sto
|
|||
}
|
||||
searchRegistriesAreEmpty := len(registries) == 0
|
||||
|
||||
// Create all of the combinations. Some registries need an additional component added, so
|
||||
// use our lookaside map to keep track of them. If there are no configured registries, we'll
|
||||
// return a name using "localhost" as the registry name.
|
||||
candidates := []string{}
|
||||
initRegistries := []string{"localhost"}
|
||||
var candidates []string
|
||||
// Set the first registry if requested.
|
||||
if firstRegistry != "" && firstRegistry != "localhost" {
|
||||
initRegistries = append([]string{firstRegistry}, initRegistries...)
|
||||
}
|
||||
for _, registry := range append(initRegistries, registries...) {
|
||||
if registry == "" {
|
||||
continue
|
||||
}
|
||||
middle := ""
|
||||
if prefix, ok := RegistryDefaultPathPrefix[registry]; ok && !strings.ContainsRune(name, '/') {
|
||||
if prefix, ok := RegistryDefaultPathPrefix[firstRegistry]; ok && !strings.ContainsRune(name, '/') {
|
||||
middle = prefix
|
||||
}
|
||||
candidate := path.Join(registry, middle, name)
|
||||
candidate := path.Join(firstRegistry, middle, name)
|
||||
candidates = append(candidates, candidate)
|
||||
}
|
||||
|
||||
// Local short-name resolution.
|
||||
namedCandidates, err := shortnames.ResolveLocally(sc, name)
|
||||
if err != nil {
|
||||
return nil, "", false, err
|
||||
}
|
||||
for _, named := range namedCandidates {
|
||||
candidates = append(candidates, named.String())
|
||||
}
|
||||
|
||||
return candidates, DefaultTransport, searchRegistriesAreEmpty, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
package util
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
|
||||
"golang.org/x/sys/unix"
|
||||
|
|
@ -19,11 +18,3 @@ func IsCgroup2UnifiedMode() (bool, error) {
|
|||
})
|
||||
return isUnified, isUnifiedErr
|
||||
}
|
||||
|
||||
func UID(st os.FileInfo) int {
|
||||
return int(st.Sys().(*syscall.Stat_t).Uid)
|
||||
}
|
||||
|
||||
func GID(st os.FileInfo) int {
|
||||
return int(st.Sys().(*syscall.Stat_t).Gid)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,3 +29,11 @@ func (h *HardlinkChecker) Add(fi os.FileInfo, name string) {
|
|||
h.hardlinks.Store(makeHardlinkDeviceAndInode(st), name)
|
||||
}
|
||||
}
|
||||
|
||||
func UID(st os.FileInfo) int {
|
||||
return int(st.Sys().(*syscall.Stat_t).Uid)
|
||||
}
|
||||
|
||||
func GID(st os.FileInfo) int {
|
||||
return int(st.Sys().(*syscall.Stat_t).Gid)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,19 +2,7 @@
|
|||
|
||||
package util
|
||||
|
||||
import (
|
||||
"os"
|
||||
)
|
||||
|
||||
// IsCgroup2UnifiedMode returns whether we are running in cgroup 2 cgroup2 mode.
|
||||
func IsCgroup2UnifiedMode() (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func UID(st os.FileInfo) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func GID(st os.FileInfo) int {
|
||||
return 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,3 +14,11 @@ func (h *HardlinkChecker) Check(fi os.FileInfo) string {
|
|||
}
|
||||
func (h *HardlinkChecker) Add(fi os.FileInfo, name string) {
|
||||
}
|
||||
|
||||
func UID(st os.FileInfo) int {
|
||||
return 0
|
||||
}
|
||||
|
||||
func GID(st os.FileInfo) int {
|
||||
return 0
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1166,6 +1166,21 @@ func computeDiffID(stream io.Reader, decompressor compression.DecompressorFunc)
|
|||
return digest.Canonical.FromReader(stream)
|
||||
}
|
||||
|
||||
// errorAnnotationReader wraps the io.Reader passed to PutBlob for annotating the error happened during read.
|
||||
// These errors are reported as PutBlob errors, so we would otherwise misleadingly attribute them to the copy destination.
|
||||
type errorAnnotationReader struct {
|
||||
reader io.Reader
|
||||
}
|
||||
|
||||
// Read annotates the error happened during read
|
||||
func (r errorAnnotationReader) Read(b []byte) (n int, err error) {
|
||||
n, err = r.reader.Read(b)
|
||||
if err != io.EOF {
|
||||
return n, errors.Wrapf(err, "error happened during read")
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
|
||||
// copyBlobFromStream copies a blob with srcInfo (with known Digest and Annotations and possibly known Size) from srcStream to dest,
|
||||
// perhaps sending a copy to an io.Writer if getOriginalLayerCopyWriter != nil,
|
||||
// perhaps compressing it if canCompress,
|
||||
|
|
@ -1335,7 +1350,7 @@ func (c *copier) copyBlobFromStream(ctx context.Context, srcStream io.Reader, sr
|
|||
}
|
||||
|
||||
// === Finally, send the layer stream to dest.
|
||||
uploadedInfo, err := c.dest.PutBlob(ctx, destStream, inputInfo, c.blobInfoCache, isConfig)
|
||||
uploadedInfo, err := c.dest.PutBlob(ctx, &errorAnnotationReader{destStream}, inputInfo, c.blobInfoCache, isConfig)
|
||||
if err != nil {
|
||||
return types.BlobInfo{}, errors.Wrap(err, "Error writing blob")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,12 +4,15 @@ import (
|
|||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/containers/image/v5/docker/reference"
|
||||
"github.com/containers/image/v5/image"
|
||||
"github.com/containers/image/v5/manifest"
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
|
|
@ -103,3 +106,47 @@ func GetRepositoryTags(ctx context.Context, sys *types.SystemContext, ref types.
|
|||
}
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// GetDigest returns the image's digest
|
||||
// Use this to optimize and avoid use of an ImageSource based on the returned digest;
|
||||
// if you are going to use an ImageSource anyway, it’s more efficient to create it first
|
||||
// and compute the digest from the value returned by GetManifest.
|
||||
// NOTE: Implemented to avoid Docker Hub API limits, and mirror configuration may be
|
||||
// ignored (but may be implemented in the future)
|
||||
func GetDigest(ctx context.Context, sys *types.SystemContext, ref types.ImageReference) (digest.Digest, error) {
|
||||
dr, ok := ref.(dockerReference)
|
||||
if !ok {
|
||||
return "", errors.Errorf("ref must be a dockerReference")
|
||||
}
|
||||
|
||||
tagOrDigest, err := dr.tagOrDigest()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
client, err := newDockerClientFromRef(sys, dr, false, "pull")
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "failed to create client")
|
||||
}
|
||||
|
||||
path := fmt.Sprintf(manifestPath, reference.Path(dr.ref), tagOrDigest)
|
||||
headers := map[string][]string{
|
||||
"Accept": manifest.DefaultRequestedManifestMIMETypes,
|
||||
}
|
||||
|
||||
res, err := client.makeRequest(ctx, "HEAD", path, headers, nil, v2Auth, nil)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return "", errors.Wrapf(registryHTTPResponseToError(res), "Error reading digest %s in %s", tagOrDigest, dr.ref.Name())
|
||||
}
|
||||
|
||||
dig, err := digest.Parse(res.Header.Get("Docker-Content-Digest"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return dig, nil
|
||||
}
|
||||
|
|
|
|||
458
vendor/github.com/containers/image/v5/pkg/shortnames/shortnames.go
generated
vendored
Normal file
458
vendor/github.com/containers/image/v5/pkg/shortnames/shortnames.go
generated
vendored
Normal file
|
|
@ -0,0 +1,458 @@
|
|||
package shortnames
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/containers/image/v5/docker/reference"
|
||||
"github.com/containers/image/v5/pkg/sysregistriesv2"
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/manifoldco/promptui"
|
||||
"github.com/opencontainers/go-digest"
|
||||
"github.com/pkg/errors"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
// IsShortName returns true if the specified input is a "short name". A "short
|
||||
// name" refers to a container image without a fully-qualified reference, and
|
||||
// is hence missing a registry (or domain). Names including a digest are not
|
||||
// short names.
|
||||
//
|
||||
// Examples:
|
||||
// * short names: "image:tag", "library/fedora"
|
||||
// * not short names: "quay.io/image", "localhost/image:tag",
|
||||
// "server.org:5000/lib/image", "image@sha256:..."
|
||||
func IsShortName(input string) bool {
|
||||
isShort, _, _ := parseUnnormalizedShortName(input)
|
||||
return isShort
|
||||
}
|
||||
|
||||
// parseUnnormalizedShortName parses the input and returns if it's short name,
|
||||
// the unnormalized reference.Named, and a parsing error.
|
||||
func parseUnnormalizedShortName(input string) (bool, reference.Named, error) {
|
||||
ref, err := reference.Parse(input)
|
||||
if err != nil {
|
||||
return false, nil, errors.Wrapf(err, "cannot parse input: %q", input)
|
||||
}
|
||||
|
||||
named, ok := ref.(reference.Named)
|
||||
if !ok {
|
||||
return true, nil, errors.Errorf("%q is not a named reference", input)
|
||||
}
|
||||
|
||||
registry := reference.Domain(named)
|
||||
if strings.ContainsAny(registry, ".:") || registry == "localhost" {
|
||||
// A final parse to make sure that docker.io references are correctly
|
||||
// normalized (e.g., docker.io/alpine to docker.io/library/alpine.
|
||||
named, err = reference.ParseNormalizedNamed(input)
|
||||
if err != nil {
|
||||
return false, nil, errors.Wrapf(err, "cannot normalize input: %q", input)
|
||||
}
|
||||
return false, named, nil
|
||||
}
|
||||
|
||||
return true, named, nil
|
||||
}
|
||||
|
||||
// splitUserInput parses the user-specified reference. Namely, it strips off
|
||||
// the tag or digest and stores it in the return values so that both can be
|
||||
// re-added to a possible resolved alias' or USRs at a later point.
|
||||
func splitUserInput(named reference.Named) (isTagged bool, isDigested bool, normalized reference.Named, tag string, digest digest.Digest) {
|
||||
normalized = named
|
||||
|
||||
tagged, isT := named.(reference.NamedTagged)
|
||||
if isT {
|
||||
isTagged = true
|
||||
tag = tagged.Tag()
|
||||
}
|
||||
|
||||
digested, isD := named.(reference.Digested)
|
||||
if isD {
|
||||
isDigested = true
|
||||
digest = digested.Digest()
|
||||
}
|
||||
|
||||
// Strip off tag/digest if present.
|
||||
normalized = reference.TrimNamed(named)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Add records the specified name-value pair as a new short-name alias to the
|
||||
// user-specific aliases.conf. It may override an existing alias for `name`.
|
||||
func Add(ctx *types.SystemContext, name string, value reference.Named) error {
|
||||
isShort, _, err := parseUnnormalizedShortName(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isShort {
|
||||
return errors.Errorf("%q is not a short name", name)
|
||||
}
|
||||
return sysregistriesv2.AddShortNameAlias(ctx, name, value.String())
|
||||
}
|
||||
|
||||
// Remove clears the short-name alias for the specified name. It throws an
|
||||
// error in case name does not exist in the machine-generated
|
||||
// short-name-alias.conf. In such case, the alias must be specified in one of
|
||||
// the registries.conf files, which is the users' responsibility.
|
||||
func Remove(ctx *types.SystemContext, name string) error {
|
||||
isShort, _, err := parseUnnormalizedShortName(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isShort {
|
||||
return errors.Errorf("%q is not a short name", name)
|
||||
}
|
||||
return sysregistriesv2.RemoveShortNameAlias(ctx, name)
|
||||
}
|
||||
|
||||
// Resolved encapsulates all data for a resolved image name.
|
||||
type Resolved struct {
|
||||
PullCandidates []PullCandidate
|
||||
|
||||
userInput reference.Named
|
||||
systemContext *types.SystemContext
|
||||
rationale rationale
|
||||
originDescription string
|
||||
}
|
||||
|
||||
func (r *Resolved) addCandidate(named reference.Named) {
|
||||
r.PullCandidates = append(r.PullCandidates, PullCandidate{named, false, r})
|
||||
}
|
||||
|
||||
func (r *Resolved) addCandidateToRecord(named reference.Named) {
|
||||
r.PullCandidates = append(r.PullCandidates, PullCandidate{named, true, r})
|
||||
}
|
||||
|
||||
// Allows to reason over pull errors and add some context information.
|
||||
// Used in (*Resolved).WrapPullError.
|
||||
type rationale int
|
||||
|
||||
const (
|
||||
// No additional context.
|
||||
rationaleNone rationale = iota
|
||||
// Resolved value is a short-name alias.
|
||||
rationaleAlias
|
||||
// Resolved value has been completed with an Unqualified Search Registry.
|
||||
rationaleUSR
|
||||
// Resolved value has been selected by the user (via the prompt).
|
||||
rationaleUserSelection
|
||||
)
|
||||
|
||||
// Description returns a human-readable description about the resolution
|
||||
// process (e.g., short-name alias, unqualified-search registries, etc.).
|
||||
// It is meant to be printed before attempting to pull the pull candidates
|
||||
// to make the short-name resolution more transparent to user.
|
||||
//
|
||||
// If the returned string is empty, it is not meant to be printed.
|
||||
func (r *Resolved) Description() string {
|
||||
switch r.rationale {
|
||||
case rationaleAlias:
|
||||
return fmt.Sprintf("Resolved short name %q to a recorded short-name alias (origin: %s)", r.userInput, r.originDescription)
|
||||
case rationaleUSR:
|
||||
return fmt.Sprintf("Completed short name %q with unqualified-search registries (origin: %s)", r.userInput, r.originDescription)
|
||||
case rationaleUserSelection, rationaleNone:
|
||||
fallthrough
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
// FormatPullErrors is a convenience function to format errors that occurred
|
||||
// while trying to pull all of the resolved pull candidates.
|
||||
//
|
||||
// Note that nil is returned if len(pullErrors) == 0. Otherwise, the amount of
|
||||
// pull errors must equal the amount of pull candidates.
|
||||
func (r *Resolved) FormatPullErrors(pullErrors []error) error {
|
||||
if len(pullErrors) >= 0 && len(pullErrors) != len(r.PullCandidates) {
|
||||
pullErrors = append(pullErrors,
|
||||
errors.Errorf("internal error: expected %d instead of %d errors for %d pull candidates",
|
||||
len(r.PullCandidates), len(pullErrors), len(r.PullCandidates)))
|
||||
}
|
||||
|
||||
switch len(pullErrors) {
|
||||
case 0:
|
||||
return nil
|
||||
case 1:
|
||||
return pullErrors[0]
|
||||
default:
|
||||
var sb strings.Builder
|
||||
sb.WriteString(fmt.Sprintf("%d errors occurred while pulling:", len(pullErrors)))
|
||||
for _, e := range pullErrors {
|
||||
sb.WriteString("\n * ")
|
||||
sb.WriteString(e.Error())
|
||||
}
|
||||
return errors.New(sb.String())
|
||||
}
|
||||
}
|
||||
|
||||
// PullCandidate is a resolved name. Once the Value has been used
|
||||
// successfully, users MUST call `(*PullCandidate).Record(..)` to possibly
|
||||
// record it as a new short-name alias.
|
||||
type PullCandidate struct {
|
||||
// Fully-qualified reference with tag or digest.
|
||||
Value reference.Named
|
||||
// Control whether to record it permanently as an alias.
|
||||
record bool
|
||||
|
||||
// Backwards pointer to the Resolved "parent".
|
||||
resolved *Resolved
|
||||
}
|
||||
|
||||
// Record may store a short-name alias for the PullCandidate.
|
||||
func (c *PullCandidate) Record() error {
|
||||
if !c.record {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Strip off tags/digests from name/value.
|
||||
name := reference.TrimNamed(c.resolved.userInput)
|
||||
value := reference.TrimNamed(c.Value)
|
||||
|
||||
if err := Add(c.resolved.systemContext, name.String(), value); err != nil {
|
||||
return errors.Wrapf(err, "error recording short-name alias (%q=%q)", c.resolved.userInput, c.Value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Resolve resolves the specified name to either one or more fully-qualified
|
||||
// image references that the short name may be *pulled* from. If the specified
|
||||
// name is already a fully-qualified reference (i.e., not a short name), it is
|
||||
// returned as is. In case, it's a short name, it's resolved according to the
|
||||
// ShortNameMode in the SystemContext (if specified) or in the registries.conf.
|
||||
//
|
||||
// Note that tags and digests are stripped from the specified name before
|
||||
// looking up an alias. Stripped off tags and digests are later on appended to
|
||||
// all candidates. If neither tag nor digest is specified, candidates are
|
||||
// normalized with the "latest" tag. PullCandidates in the returned value may
|
||||
// be empty if there is no matching alias and no unqualified-search registries
|
||||
// are configured.
|
||||
//
|
||||
// Note that callers *must* call `(PullCandidate).Record` after a returned
|
||||
// item has been pulled successfully; this callback will record a new
|
||||
// short-name alias (depending on the specified short-name mode).
|
||||
//
|
||||
// Furthermore, before attempting to pull callers *should* call
|
||||
// `(Resolved).Description` and afterwards use
|
||||
// `(Resolved).FormatPullErrors` in case of pull errors.
|
||||
func Resolve(ctx *types.SystemContext, name string) (*Resolved, error) {
|
||||
resolved := &Resolved{}
|
||||
|
||||
// Create a copy of the system context to make it usable beyond this
|
||||
// function call.
|
||||
var sys *types.SystemContext
|
||||
if ctx != nil {
|
||||
sys = &(*ctx)
|
||||
}
|
||||
resolved.systemContext = ctx
|
||||
|
||||
// Detect which mode we're running in.
|
||||
mode, err := sysregistriesv2.GetShortNameMode(sys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Sanity check the short-name mode.
|
||||
switch mode {
|
||||
case types.ShortNameModeDisabled, types.ShortNameModePermissive, types.ShortNameModeEnforcing:
|
||||
// We're good.
|
||||
default:
|
||||
return nil, errors.Errorf("unsupported short-name mode (%v)", mode)
|
||||
}
|
||||
|
||||
isShort, shortRef, err := parseUnnormalizedShortName(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !isShort { // no short name
|
||||
named := reference.TagNameOnly(shortRef) // Make sure to add ":latest" if needed
|
||||
resolved.addCandidate(named)
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
// Strip off the tag to normalize the short name for looking it up in
|
||||
// the config files.
|
||||
isTagged, isDigested, shortNameRepo, tag, digest := splitUserInput(shortRef)
|
||||
resolved.userInput = shortNameRepo
|
||||
|
||||
// If there's already an alias, use it.
|
||||
namedAlias, aliasOriginDescription, err := sysregistriesv2.ResolveShortNameAlias(sys, shortNameRepo.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Always use an alias if present.
|
||||
if namedAlias != nil {
|
||||
if isTagged {
|
||||
namedAlias, err = reference.WithTag(namedAlias, tag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if isDigested {
|
||||
namedAlias, err = reference.WithDigest(namedAlias, digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// Make sure to add ":latest" if needed
|
||||
namedAlias = reference.TagNameOnly(namedAlias)
|
||||
|
||||
resolved.addCandidate(namedAlias)
|
||||
resolved.rationale = rationaleAlias
|
||||
resolved.originDescription = aliasOriginDescription
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
resolved.rationale = rationaleUSR
|
||||
|
||||
// Query the registry for unqualified-search registries.
|
||||
unqualifiedSearchRegistries, usrConfig, err := sysregistriesv2.UnqualifiedSearchRegistriesWithOrigin(sys)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resolved.originDescription = usrConfig
|
||||
|
||||
for _, reg := range unqualifiedSearchRegistries {
|
||||
named, err := reference.ParseNormalizedNamed(fmt.Sprintf("%s/%s", reg, name))
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error creating reference with unqualified-search registry %q", reg)
|
||||
}
|
||||
// Make sure to add ":latest" if needed
|
||||
named = reference.TagNameOnly(named)
|
||||
|
||||
resolved.addCandidate(named)
|
||||
}
|
||||
|
||||
// If we're running in disabled, return the candidates without
|
||||
// prompting (and without recording).
|
||||
if mode == types.ShortNameModeDisabled {
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
// If we have only one candidate, there's no ambiguity. In case of an
|
||||
// empty candidate slices, callers can implement custom logic or raise
|
||||
// an error.
|
||||
if len(resolved.PullCandidates) <= 1 {
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
// If we don't have a TTY, act according to the mode.
|
||||
if !terminal.IsTerminal(int(os.Stdout.Fd())) || !terminal.IsTerminal(int(os.Stdin.Fd())) {
|
||||
switch mode {
|
||||
case types.ShortNameModePermissive:
|
||||
// Permissive falls back to using all candidates.
|
||||
return resolved, nil
|
||||
case types.ShortNameModeEnforcing:
|
||||
// Enforcing errors out without a prompt.
|
||||
return nil, errors.New("short-name resolution enforced but cannot prompt without a TTY")
|
||||
default:
|
||||
// We should not end up here.
|
||||
return nil, errors.Errorf("unexpected short-name mode (%v) during resolution", mode)
|
||||
}
|
||||
}
|
||||
|
||||
// We have a TTY, and can prompt the user with a selection of all
|
||||
// possible candidates.
|
||||
strCandidates := []string{}
|
||||
for _, candidate := range resolved.PullCandidates {
|
||||
strCandidates = append(strCandidates, candidate.Value.String())
|
||||
}
|
||||
prompt := promptui.Select{
|
||||
Label: "Please select an image",
|
||||
Items: strCandidates,
|
||||
HideHelp: true, // do not show navigation help
|
||||
}
|
||||
|
||||
_, selection, err := prompt.Run()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
named, err := reference.ParseNormalizedNamed(selection)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "selection %q is not a valid reference", selection)
|
||||
}
|
||||
|
||||
resolved.PullCandidates = nil
|
||||
resolved.addCandidateToRecord(named)
|
||||
resolved.rationale = rationaleUserSelection
|
||||
|
||||
return resolved, nil
|
||||
}
|
||||
|
||||
// ResolveLocally resolves the specified name to either one or more local
|
||||
// images. If the specified name is already a fully-qualified reference (i.e.,
|
||||
// not a short name), it is returned as is. In case, it's a short name, the
|
||||
// returned slice of named references looks as follows:
|
||||
//
|
||||
// 1) If present, the short-name alias
|
||||
// 2) "localhost/" as used by many container engines such as Podman and Buildah
|
||||
// 3) Unqualified-search registries from the registries.conf files
|
||||
//
|
||||
// Note that tags and digests are stripped from the specified name before
|
||||
// looking up an alias. Stripped off tags and digests are later on appended to
|
||||
// all candidates. If neither tag nor digest is specified, candidates are
|
||||
// normalized with the "latest" tag. The returned slice contains at least one
|
||||
// item.
|
||||
func ResolveLocally(ctx *types.SystemContext, name string) ([]reference.Named, error) {
|
||||
isShort, shortRef, err := parseUnnormalizedShortName(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !isShort { // no short name
|
||||
named := reference.TagNameOnly(shortRef) // Make sure to add ":latest" if needed
|
||||
return []reference.Named{named}, nil
|
||||
}
|
||||
|
||||
var candidates []reference.Named
|
||||
|
||||
// Strip off the tag to normalize the short name for looking it up in
|
||||
// the config files.
|
||||
isTagged, isDigested, shortNameRepo, tag, digest := splitUserInput(shortRef)
|
||||
|
||||
// If there's already an alias, use it.
|
||||
namedAlias, _, err := sysregistriesv2.ResolveShortNameAlias(ctx, shortNameRepo.String())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if namedAlias != nil {
|
||||
if isTagged {
|
||||
namedAlias, err = reference.WithTag(namedAlias, tag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if isDigested {
|
||||
namedAlias, err = reference.WithDigest(namedAlias, digest)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
// Make sure to add ":latest" if needed
|
||||
namedAlias = reference.TagNameOnly(namedAlias)
|
||||
|
||||
candidates = append(candidates, namedAlias)
|
||||
}
|
||||
|
||||
// Query the registry for unqualified-search registries.
|
||||
unqualifiedSearchRegistries, err := sysregistriesv2.UnqualifiedSearchRegistries(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Note that "localhost" has precedence over the unqualified-search registries.
|
||||
for _, reg := range append([]string{"localhost"}, unqualifiedSearchRegistries...) {
|
||||
named, err := reference.ParseNormalizedNamed(fmt.Sprintf("%s/%s", reg, name))
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error creating reference with unqualified-search registry %q", reg)
|
||||
}
|
||||
// Make sure to add ":latest" if needed
|
||||
named = reference.TagNameOnly(named)
|
||||
|
||||
candidates = append(candidates, named)
|
||||
}
|
||||
|
||||
return candidates, nil
|
||||
}
|
||||
328
vendor/github.com/containers/image/v5/pkg/sysregistriesv2/shortnames.go
generated
vendored
Normal file
328
vendor/github.com/containers/image/v5/pkg/sysregistriesv2/shortnames.go
generated
vendored
Normal file
|
|
@ -0,0 +1,328 @@
|
|||
package sysregistriesv2
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/containers/image/v5/docker/reference"
|
||||
"github.com/containers/image/v5/types"
|
||||
"github.com/containers/storage/pkg/lockfile"
|
||||
"github.com/docker/docker/pkg/homedir"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// defaultShortNameMode is the default mode of registries.conf files if the
|
||||
// corresponding field is left empty.
|
||||
const defaultShortNameMode = types.ShortNameModePermissive
|
||||
|
||||
// userShortNamesFile is the user-specific config file to store aliases.
|
||||
var userShortNamesFile = filepath.FromSlash("containers/short-name-aliases.conf")
|
||||
|
||||
// shortNameAliasesConfPath returns the path to the machine-generated
|
||||
// short-name-aliases.conf file.
|
||||
func shortNameAliasesConfPath(ctx *types.SystemContext) (string, error) {
|
||||
if ctx != nil && len(ctx.UserShortNameAliasConfPath) > 0 {
|
||||
return ctx.UserShortNameAliasConfPath, nil
|
||||
}
|
||||
|
||||
configHome, err := homedir.GetConfigHome()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return filepath.Join(configHome, userShortNamesFile), nil
|
||||
}
|
||||
|
||||
// shortNameAliasConf is a subset of the `V2RegistriesConf` format. It's used in the
|
||||
// software-maintained `userShortNamesFile`.
|
||||
type shortNameAliasConf struct {
|
||||
// A map for aliasing short names to their fully-qualified image
|
||||
// reference counter parts.
|
||||
// Note that Aliases is niled after being loaded from a file.
|
||||
Aliases map[string]string `toml:"aliases"`
|
||||
}
|
||||
|
||||
// alias combines the parsed value of an alias with the config file it has been
|
||||
// specified in. The config file is crucial for an improved user experience
|
||||
// such that users are able to resolve potential pull errors.
|
||||
type alias struct {
|
||||
// The parsed value of an alias. May be nil if set to "" in a config.
|
||||
value reference.Named
|
||||
// The config file the alias originates from.
|
||||
configOrigin string
|
||||
}
|
||||
|
||||
// shortNameAliasCache is the result of parsing shortNameAliasConf,
|
||||
// pre-processed for faster usage.
|
||||
type shortNameAliasCache struct {
|
||||
// Note that an alias value may be nil iff it's set as an empty string
|
||||
// in the config.
|
||||
namedAliases map[string]alias
|
||||
}
|
||||
|
||||
// ResolveShortNameAlias performs an alias resolution of the specified name.
|
||||
// The user-specific short-name-aliases.conf has precedence over aliases in the
|
||||
// assembled registries.conf. It returns the possibly resolved alias or nil, a
|
||||
// human-readable description of the config where the alias is specified, and
|
||||
// an error. The origin of the config file is crucial for an improved user
|
||||
// experience such that users are able to resolve potential pull errors.
|
||||
// Almost all callers should use pkg/shortnames instead.
|
||||
//
|
||||
// Note that it’s the caller’s responsibility to pass only a repository
|
||||
// (reference.IsNameOnly) as the short name.
|
||||
func ResolveShortNameAlias(ctx *types.SystemContext, name string) (reference.Named, string, error) {
|
||||
if err := validateShortName(name); err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
confPath, lock, err := shortNameAliasesConfPathAndLock(ctx)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// Acquire the lock as a reader to allow for multiple routines in the
|
||||
// same process space to read simultaneously.
|
||||
lock.RLock()
|
||||
defer lock.Unlock()
|
||||
|
||||
_, aliasCache, err := loadShortNameAliasConf(confPath)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
// First look up the short-name-aliases.conf. Note that a value may be
|
||||
// nil iff it's set as an empty string in the config.
|
||||
alias, resolved := aliasCache.namedAliases[name]
|
||||
if resolved {
|
||||
return alias.value, alias.configOrigin, nil
|
||||
}
|
||||
|
||||
config, err := getConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
alias, resolved = config.aliasCache.namedAliases[name]
|
||||
if resolved {
|
||||
return alias.value, alias.configOrigin, nil
|
||||
}
|
||||
return nil, "", nil
|
||||
}
|
||||
|
||||
// editShortNameAlias loads the aliases.conf file and changes it. If value is
|
||||
// set, it adds the name-value pair as a new alias. Otherwise, it will remove
|
||||
// name from the config.
|
||||
func editShortNameAlias(ctx *types.SystemContext, name string, value *string) error {
|
||||
if err := validateShortName(name); err != nil {
|
||||
return err
|
||||
}
|
||||
if value != nil {
|
||||
if _, err := parseShortNameValue(*value); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
confPath, lock, err := shortNameAliasesConfPathAndLock(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Acquire the lock as a writer to prevent data corruption.
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
// Load the short-name-alias.conf, add the specified name-value pair,
|
||||
// and write it back to the file.
|
||||
conf, _, err := loadShortNameAliasConf(confPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if conf.Aliases == nil { // Ensure we have a map to update.
|
||||
conf.Aliases = make(map[string]string)
|
||||
}
|
||||
if value != nil {
|
||||
conf.Aliases[name] = *value
|
||||
} else {
|
||||
// If the name does not exist, throw an error.
|
||||
if _, exists := conf.Aliases[name]; !exists {
|
||||
return errors.Errorf("short-name alias %q not found in %q: please check registries.conf files", name, confPath)
|
||||
}
|
||||
|
||||
delete(conf.Aliases, name)
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(confPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
encoder := toml.NewEncoder(f)
|
||||
return encoder.Encode(conf)
|
||||
}
|
||||
|
||||
// AddShortNameAlias adds the specified name-value pair as a new alias to the
|
||||
// user-specific aliases.conf. It may override an existing alias for `name`.
|
||||
//
|
||||
// Note that it’s the caller’s responsibility to pass only a repository
|
||||
// (reference.IsNameOnly) as the short name.
|
||||
func AddShortNameAlias(ctx *types.SystemContext, name string, value string) error {
|
||||
return editShortNameAlias(ctx, name, &value)
|
||||
}
|
||||
|
||||
// RemoveShortNameAlias clears the alias for the specified name. It throws an
|
||||
// error in case name does not exist in the machine-generated
|
||||
// short-name-alias.conf. In such case, the alias must be specified in one of
|
||||
// the registries.conf files, which is the users' responsibility.
|
||||
//
|
||||
// Note that it’s the caller’s responsibility to pass only a repository
|
||||
// (reference.IsNameOnly) as the short name.
|
||||
func RemoveShortNameAlias(ctx *types.SystemContext, name string) error {
|
||||
return editShortNameAlias(ctx, name, nil)
|
||||
}
|
||||
|
||||
// parseShortNameValue parses the specified alias into a reference.Named. The alias is
|
||||
// expected to not be tagged or carry a digest and *must* include a
|
||||
// domain/registry.
|
||||
//
|
||||
// Note that the returned reference is always normalized.
|
||||
func parseShortNameValue(alias string) (reference.Named, error) {
|
||||
ref, err := reference.Parse(alias)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error parsing alias %q", alias)
|
||||
}
|
||||
|
||||
if _, ok := ref.(reference.Digested); ok {
|
||||
return nil, errors.Errorf("invalid alias %q: must not contain digest", alias)
|
||||
}
|
||||
|
||||
if _, ok := ref.(reference.Tagged); ok {
|
||||
return nil, errors.Errorf("invalid alias %q: must not contain tag", alias)
|
||||
}
|
||||
|
||||
named, ok := ref.(reference.Named)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("invalid alias %q: must contain registry and repository", alias)
|
||||
}
|
||||
|
||||
registry := reference.Domain(named)
|
||||
if !(strings.ContainsAny(registry, ".:") || registry == "localhost") {
|
||||
return nil, errors.Errorf("invalid alias %q: must contain registry and repository", alias)
|
||||
}
|
||||
|
||||
// A final parse to make sure that docker.io references are correctly
|
||||
// normalized (e.g., docker.io/alpine to docker.io/library/alpine.
|
||||
named, err = reference.ParseNormalizedNamed(alias)
|
||||
return named, err
|
||||
}
|
||||
|
||||
// validateShortName parses the specified `name` of an alias (i.e., the left-hand
|
||||
// side) and checks if it's a short name and does not include a tag or digest.
|
||||
func validateShortName(name string) error {
|
||||
repo, err := reference.Parse(name)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "cannot parse short name: %q", name)
|
||||
}
|
||||
|
||||
if _, ok := repo.(reference.Digested); ok {
|
||||
return errors.Errorf("invalid short name %q: must not contain digest", name)
|
||||
}
|
||||
|
||||
if _, ok := repo.(reference.Tagged); ok {
|
||||
return errors.Errorf("invalid short name %q: must not contain tag", name)
|
||||
}
|
||||
|
||||
named, ok := repo.(reference.Named)
|
||||
if !ok {
|
||||
return errors.Errorf("invalid short name %q: no name", name)
|
||||
}
|
||||
|
||||
registry := reference.Domain(named)
|
||||
if strings.ContainsAny(registry, ".:") || registry == "localhost" {
|
||||
return errors.Errorf("invalid short name %q: must not contain registry", name)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// newShortNameAliasCache parses shortNameAliasConf and returns the corresponding internal
|
||||
// representation.
|
||||
func newShortNameAliasCache(path string, conf *shortNameAliasConf) (*shortNameAliasCache, error) {
|
||||
res := shortNameAliasCache{
|
||||
namedAliases: make(map[string]alias),
|
||||
}
|
||||
errs := []error{}
|
||||
for name, value := range conf.Aliases {
|
||||
if err := validateShortName(name); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
|
||||
// Empty right-hand side values in config files allow to reset
|
||||
// an alias in a previously loaded config. This way, drop-in
|
||||
// config files from registries.conf.d can reset potentially
|
||||
// malconfigured aliases.
|
||||
if value == "" {
|
||||
res.namedAliases[name] = alias{nil, path}
|
||||
continue
|
||||
}
|
||||
|
||||
named, err := parseShortNameValue(value)
|
||||
if err != nil {
|
||||
// We want to report *all* malformed entries to avoid a
|
||||
// whack-a-mole for the user.
|
||||
errs = append(errs, err)
|
||||
} else {
|
||||
res.namedAliases[name] = alias{named, path}
|
||||
}
|
||||
}
|
||||
if len(errs) > 0 {
|
||||
err := errs[0]
|
||||
for i := 1; i < len(errs); i++ {
|
||||
err = errors.Wrapf(err, "%v\n", errs[i])
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
// updateWithConfigurationFrom updates c with configuration from updates.
|
||||
// In case of conflict, updates is preferred.
|
||||
func (c *shortNameAliasCache) updateWithConfigurationFrom(updates *shortNameAliasCache) {
|
||||
for name, value := range updates.namedAliases {
|
||||
c.namedAliases[name] = value
|
||||
}
|
||||
}
|
||||
|
||||
func loadShortNameAliasConf(confPath string) (*shortNameAliasConf, *shortNameAliasCache, error) {
|
||||
conf := shortNameAliasConf{}
|
||||
|
||||
_, err := toml.DecodeFile(confPath, &conf)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
// It's okay if the config doesn't exist. Other errors are not.
|
||||
return nil, nil, errors.Wrapf(err, "error loading short-name aliases config file %q", confPath)
|
||||
}
|
||||
|
||||
// Even if we don’t always need the cache, doing so validates the machine-generated config. The
|
||||
// file could still be corrupted by another process or user.
|
||||
cache, err := newShortNameAliasCache(confPath, &conf)
|
||||
if err != nil {
|
||||
return nil, nil, errors.Wrapf(err, "error loading short-name aliases config file %q", confPath)
|
||||
}
|
||||
|
||||
return &conf, cache, nil
|
||||
}
|
||||
|
||||
func shortNameAliasesConfPathAndLock(ctx *types.SystemContext) (string, lockfile.Locker, error) {
|
||||
shortNameAliasesConfPath, err := shortNameAliasesConfPath(ctx)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
// Make sure the path to file exists.
|
||||
if err := os.MkdirAll(filepath.Dir(shortNameAliasesConfPath), 0700); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
lockPath := shortNameAliasesConfPath + ".lock"
|
||||
locker, err := lockfile.GetLockfile(lockPath)
|
||||
return shortNameAliasesConfPath, locker, err
|
||||
}
|
||||
252
vendor/github.com/containers/image/v5/pkg/sysregistriesv2/system_registries_v2.go
generated
vendored
252
vendor/github.com/containers/image/v5/pkg/sysregistriesv2/system_registries_v2.go
generated
vendored
|
|
@ -154,6 +154,19 @@ type V2RegistriesConf struct {
|
|||
Registries []Registry `toml:"registry"`
|
||||
// An array of host[:port] (not prefix!) entries to use for resolving unqualified image references
|
||||
UnqualifiedSearchRegistries []string `toml:"unqualified-search-registries"`
|
||||
|
||||
// ShortNameMode defines how short-name resolution should be handled by
|
||||
// _consumers_ of this package. Depending on the mode, the user should
|
||||
// be prompted with a choice of using one of the unqualified-search
|
||||
// registries when referring to a short name.
|
||||
//
|
||||
// Valid modes are: * "prompt": prompt if stdout is a TTY, otherwise
|
||||
// use all unqualified-search registries * "enforcing": always prompt
|
||||
// and error if stdout is not a TTY * "disabled": do not prompt and
|
||||
// potentially use all unqualified-search registries
|
||||
ShortNameMode string `toml:"short-name-mode"`
|
||||
|
||||
shortNameAliasConf
|
||||
}
|
||||
|
||||
// Nonempty returns true if config contains at least one configuration entry.
|
||||
|
|
@ -162,10 +175,23 @@ func (config *V2RegistriesConf) Nonempty() bool {
|
|||
len(config.UnqualifiedSearchRegistries) != 0)
|
||||
}
|
||||
|
||||
// tomlConfig is the data type used to unmarshal the toml config.
|
||||
type tomlConfig struct {
|
||||
V2RegistriesConf
|
||||
V1RegistriesConf // for backwards compatibility with sysregistries v1
|
||||
// parsedConfig is the result of parsing, and possibly merging, configuration files;
|
||||
// it is the boundary between the process of reading+ingesting the files, and
|
||||
// later interpreting the configuraiton based on caller’s requests.
|
||||
type parsedConfig struct {
|
||||
// NOTE: Update also parsedConfig.updateWithConfigurationFrom!
|
||||
|
||||
// partialV2 must continue to exist to maintain the return value of TryUpdatingCache
|
||||
// for compatibility with existing callers.
|
||||
// We store the authoritative Registries and UnqualifiedSearchRegistries values there as well.
|
||||
partialV2 V2RegistriesConf
|
||||
// Absolute path to the configuration file that set the UnqualifiedSearchRegistries.
|
||||
unqualifiedSearchRegistriesOrigin string
|
||||
// Result of parsing of partialV2.ShortNameMode.
|
||||
// NOTE: May be ShortNameModeInvalid to represent ShortNameMode == "" in intermediate values;
|
||||
// the full configuration in configCache / getConfig() always contains a valid value.
|
||||
shortNameMode types.ShortNameMode
|
||||
aliasCache *shortNameAliasCache
|
||||
}
|
||||
|
||||
// InvalidRegistries represents an invalid registry configurations. An example
|
||||
|
|
@ -254,7 +280,7 @@ var anchoredDomainRegexp = regexp.MustCompile("^" + reference.DomainRegexp.Strin
|
|||
|
||||
// postProcess checks the consistency of all the configuration, looks for conflicts,
|
||||
// and normalizes the configuration (e.g., sets the Prefix to Location if not set).
|
||||
func (config *V2RegistriesConf) postProcess() error {
|
||||
func (config *V2RegistriesConf) postProcessRegistries() error {
|
||||
regMap := make(map[string][]*Registry)
|
||||
|
||||
for i := range config.Registries {
|
||||
|
|
@ -301,6 +327,7 @@ func (config *V2RegistriesConf) postProcess() error {
|
|||
msg := fmt.Sprintf("registry '%s' is defined multiple times with conflicting 'insecure' setting", reg.Location)
|
||||
return &InvalidRegistries{s: msg}
|
||||
}
|
||||
|
||||
if reg.Blocked != other.Blocked {
|
||||
msg := fmt.Sprintf("registry '%s' is defined multiple times with conflicting 'blocked' setting", reg.Location)
|
||||
return &InvalidRegistries{s: msg}
|
||||
|
|
@ -323,16 +350,25 @@ func (config *V2RegistriesConf) postProcess() error {
|
|||
// rendering later items with the same prefix non-existent. We cannot error
|
||||
// out anymore as this might break existing users, so let's just ignore them
|
||||
// to guarantee that the same prefix exists only once.
|
||||
knownPrefixes := make(map[string]bool)
|
||||
uniqueRegistries := []Registry{}
|
||||
//
|
||||
// As a side effect of parsedConfig.updateWithConfigurationFrom, the Registries slice
|
||||
// is always sorted. To be consistent in situations where it is not called (no drop-ins),
|
||||
// sort it here as well.
|
||||
prefixes := []string{}
|
||||
uniqueRegistries := make(map[string]Registry)
|
||||
for i := range config.Registries {
|
||||
// TODO: should we warn if we see the same prefix being used multiple times?
|
||||
if _, exists := knownPrefixes[config.Registries[i].Prefix]; !exists {
|
||||
knownPrefixes[config.Registries[i].Prefix] = true
|
||||
uniqueRegistries = append(uniqueRegistries, config.Registries[i])
|
||||
prefix := config.Registries[i].Prefix
|
||||
if _, exists := uniqueRegistries[prefix]; !exists {
|
||||
uniqueRegistries[prefix] = config.Registries[i]
|
||||
prefixes = append(prefixes, prefix)
|
||||
}
|
||||
}
|
||||
config.Registries = uniqueRegistries
|
||||
sort.Strings(prefixes)
|
||||
config.Registries = []Registry{}
|
||||
for _, prefix := range prefixes {
|
||||
config.Registries = append(config.Registries, uniqueRegistries[prefix])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -385,6 +421,7 @@ func newConfigWrapper(ctx *types.SystemContext) configWrapper {
|
|||
} else {
|
||||
wrapper.userConfigDirPath = userRegistriesDirPath
|
||||
}
|
||||
|
||||
return wrapper
|
||||
} else if ctx != nil && ctx.RootForImplicitAbsolutePaths != "" {
|
||||
wrapper.configPath = filepath.Join(ctx.RootForImplicitAbsolutePaths, systemRegistriesConfPath)
|
||||
|
|
@ -426,7 +463,7 @@ var configMutex = sync.Mutex{}
|
|||
// configCache caches already loaded configs with config paths as keys and is
|
||||
// used to avoid redundantly parsing configs. Concurrent accesses to the cache
|
||||
// are synchronized via configMutex.
|
||||
var configCache = make(map[configWrapper]*V2RegistriesConf)
|
||||
var configCache = make(map[configWrapper]*parsedConfig)
|
||||
|
||||
// InvalidateCache invalidates the registry cache. This function is meant to be
|
||||
// used for long-running processes that need to reload potential changes made to
|
||||
|
|
@ -434,11 +471,11 @@ var configCache = make(map[configWrapper]*V2RegistriesConf)
|
|||
func InvalidateCache() {
|
||||
configMutex.Lock()
|
||||
defer configMutex.Unlock()
|
||||
configCache = make(map[configWrapper]*V2RegistriesConf)
|
||||
configCache = make(map[configWrapper]*parsedConfig)
|
||||
}
|
||||
|
||||
// getConfig returns the config object corresponding to ctx, loading it if it is not yet cached.
|
||||
func getConfig(ctx *types.SystemContext) (*V2RegistriesConf, error) {
|
||||
func getConfig(ctx *types.SystemContext) (*parsedConfig, error) {
|
||||
wrapper := newConfigWrapper(ctx)
|
||||
configMutex.Lock()
|
||||
if config, inCache := configCache[wrapper]; inCache {
|
||||
|
|
@ -504,27 +541,37 @@ func dropInConfigs(wrapper configWrapper) ([]string, error) {
|
|||
// TryUpdatingCache loads the configuration from the provided `SystemContext`
|
||||
// without using the internal cache. On success, the loaded configuration will
|
||||
// be added into the internal registry cache.
|
||||
// It returns the resulting configuration; this is DEPRECATED and may not correctly
|
||||
// reflect any future data handled by this package.
|
||||
func TryUpdatingCache(ctx *types.SystemContext) (*V2RegistriesConf, error) {
|
||||
return tryUpdatingCache(ctx, newConfigWrapper(ctx))
|
||||
config, err := tryUpdatingCache(ctx, newConfigWrapper(ctx))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &config.partialV2, err
|
||||
}
|
||||
|
||||
// tryUpdatingCache implements TryUpdatingCache with an additional configWrapper
|
||||
// argument to avoid redundantly calculating the config paths.
|
||||
func tryUpdatingCache(ctx *types.SystemContext, wrapper configWrapper) (*V2RegistriesConf, error) {
|
||||
func tryUpdatingCache(ctx *types.SystemContext, wrapper configWrapper) (*parsedConfig, error) {
|
||||
configMutex.Lock()
|
||||
defer configMutex.Unlock()
|
||||
|
||||
// load the config
|
||||
config := &tomlConfig{}
|
||||
if err := config.loadConfig(wrapper.configPath, false); err != nil {
|
||||
config, err := loadConfigFile(wrapper.configPath, false)
|
||||
if err != nil {
|
||||
// Continue with an empty []Registry if we use the default config, which
|
||||
// implies that the config path of the SystemContext isn't set.
|
||||
//
|
||||
// Note: if ctx.SystemRegistriesConfPath points to the default config,
|
||||
// we will still return an error.
|
||||
if os.IsNotExist(err) && (ctx == nil || ctx.SystemRegistriesConfPath == "") {
|
||||
config = &tomlConfig{}
|
||||
config.V2RegistriesConf = V2RegistriesConf{Registries: []Registry{}}
|
||||
config = &parsedConfig{}
|
||||
config.partialV2 = V2RegistriesConf{Registries: []Registry{}}
|
||||
config.aliasCache, err = newShortNameAliasCache("", &shortNameAliasConf{})
|
||||
if err != nil {
|
||||
return nil, err // Should never happen
|
||||
}
|
||||
} else {
|
||||
return nil, errors.Wrapf(err, "error loading registries configuration %q", wrapper.configPath)
|
||||
}
|
||||
|
|
@ -537,16 +584,20 @@ func tryUpdatingCache(ctx *types.SystemContext, wrapper configWrapper) (*V2Regis
|
|||
}
|
||||
for _, path := range dinConfigs {
|
||||
// Enforce v2 format for drop-in-configs.
|
||||
if err := config.loadConfig(path, true); err != nil {
|
||||
dropIn, err := loadConfigFile(path, true)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "error loading drop-in registries configuration %q", path)
|
||||
}
|
||||
config.updateWithConfigurationFrom(dropIn)
|
||||
}
|
||||
|
||||
v2Config := &config.V2RegistriesConf
|
||||
if config.shortNameMode == types.ShortNameModeInvalid {
|
||||
config.shortNameMode = defaultShortNameMode
|
||||
}
|
||||
|
||||
// populate the cache
|
||||
configCache[wrapper] = v2Config
|
||||
return v2Config, nil
|
||||
configCache[wrapper] = config
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// GetRegistries loads and returns the registries specified in the config.
|
||||
|
|
@ -557,17 +608,53 @@ func GetRegistries(ctx *types.SystemContext) ([]Registry, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return config.Registries, nil
|
||||
return config.partialV2.Registries, nil
|
||||
}
|
||||
|
||||
// UnqualifiedSearchRegistries returns a list of host[:port] entries to try
|
||||
// for unqualified image search, in the returned order)
|
||||
func UnqualifiedSearchRegistries(ctx *types.SystemContext) ([]string, error) {
|
||||
registries, _, err := UnqualifiedSearchRegistriesWithOrigin(ctx)
|
||||
return registries, err
|
||||
}
|
||||
|
||||
// UnqualifiedSearchRegistriesWithOrigin returns a list of host[:port] entries
|
||||
// to try for unqualified image search, in the returned order. It also returns
|
||||
// a human-readable description of where these entries are specified (e.g., a
|
||||
// registries.conf file).
|
||||
func UnqualifiedSearchRegistriesWithOrigin(ctx *types.SystemContext) ([]string, string, error) {
|
||||
config, err := getConfig(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return nil, "", err
|
||||
}
|
||||
return config.UnqualifiedSearchRegistries, nil
|
||||
return config.partialV2.UnqualifiedSearchRegistries, config.unqualifiedSearchRegistriesOrigin, nil
|
||||
}
|
||||
|
||||
// parseShortNameMode translates the string into well-typed
|
||||
// types.ShortNameMode.
|
||||
func parseShortNameMode(mode string) (types.ShortNameMode, error) {
|
||||
switch mode {
|
||||
case "disabled":
|
||||
return types.ShortNameModeDisabled, nil
|
||||
case "enforcing":
|
||||
return types.ShortNameModeEnforcing, nil
|
||||
case "permissive":
|
||||
return types.ShortNameModePermissive, nil
|
||||
default:
|
||||
return types.ShortNameModeInvalid, errors.Errorf("invalid short-name mode: %q", mode)
|
||||
}
|
||||
}
|
||||
|
||||
// GetShortNameMode returns the configured types.ShortNameMode.
|
||||
func GetShortNameMode(ctx *types.SystemContext) (types.ShortNameMode, error) {
|
||||
if ctx != nil && ctx.ShortNameMode != nil {
|
||||
return *ctx.ShortNameMode, nil
|
||||
}
|
||||
config, err := getConfig(ctx)
|
||||
if err != nil {
|
||||
return -1, err
|
||||
}
|
||||
return config.shortNameMode, err
|
||||
}
|
||||
|
||||
// refMatchesPrefix returns true iff ref,
|
||||
|
|
@ -609,7 +696,7 @@ func FindRegistry(ctx *types.SystemContext, ref string) (*Registry, error) {
|
|||
|
||||
reg := Registry{}
|
||||
prefixLen := 0
|
||||
for _, r := range config.Registries {
|
||||
for _, r := range config.partialV2.Registries {
|
||||
if refMatchesPrefix(ref, r.Prefix) {
|
||||
length := len(r.Prefix)
|
||||
if length > prefixLen {
|
||||
|
|
@ -624,55 +711,87 @@ func FindRegistry(ctx *types.SystemContext, ref string) (*Registry, error) {
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
// loadConfig loads and unmarshals the configuration at the specified path. Note
|
||||
// that v1 configs are translated into v2 and are cleared. Use forceV2 if the
|
||||
// config must in the v2 format.
|
||||
//
|
||||
// Note that specified fields in path will replace already set fields in the
|
||||
// tomlConfig. Only the [[registry]] tables are merged by prefix.
|
||||
func (c *tomlConfig) loadConfig(path string, forceV2 bool) error {
|
||||
// loadConfigFile loads and unmarshals a single config file.
|
||||
// Use forceV2 if the config must in the v2 format.
|
||||
func loadConfigFile(path string, forceV2 bool) (*parsedConfig, error) {
|
||||
logrus.Debugf("Loading registries configuration %q", path)
|
||||
|
||||
// Save the registries before decoding the file where they could be lost.
|
||||
// We merge them later again.
|
||||
registryMap := make(map[string]Registry)
|
||||
for i := range c.Registries {
|
||||
registryMap[c.Registries[i].Prefix] = c.Registries[i]
|
||||
// tomlConfig allows us to unmarshal either V1 or V2 simultaneously.
|
||||
type tomlConfig struct {
|
||||
V2RegistriesConf
|
||||
V1RegistriesConf // for backwards compatibility with sysregistries v1
|
||||
}
|
||||
|
||||
// Load the tomlConfig. Note that `DecodeFile` will overwrite set fields.
|
||||
c.Registries = nil // important to clear the memory to prevent us from overlapping fields
|
||||
_, err := toml.DecodeFile(path, c)
|
||||
var combinedTOML tomlConfig
|
||||
_, err := toml.DecodeFile(path, &combinedTOML)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.V1RegistriesConf.Nonempty() {
|
||||
if combinedTOML.V1RegistriesConf.Nonempty() {
|
||||
// Enforce the v2 format if requested.
|
||||
if forceV2 {
|
||||
return &InvalidRegistries{s: "registry must be in v2 format but is in v1"}
|
||||
return nil, &InvalidRegistries{s: "registry must be in v2 format but is in v1"}
|
||||
}
|
||||
|
||||
// Convert a v1 config into a v2 config.
|
||||
if c.V2RegistriesConf.Nonempty() {
|
||||
return &InvalidRegistries{s: "mixing sysregistry v1/v2 is not supported"}
|
||||
if combinedTOML.V2RegistriesConf.Nonempty() {
|
||||
return nil, &InvalidRegistries{s: "mixing sysregistry v1/v2 is not supported"}
|
||||
}
|
||||
v2, err := c.V1RegistriesConf.ConvertToV2()
|
||||
converted, err := combinedTOML.V1RegistriesConf.ConvertToV2()
|
||||
if err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
c.V1RegistriesConf = V1RegistriesConf{}
|
||||
c.V2RegistriesConf = *v2
|
||||
combinedTOML.V1RegistriesConf = V1RegistriesConf{}
|
||||
combinedTOML.V2RegistriesConf = *converted
|
||||
}
|
||||
|
||||
res := parsedConfig{partialV2: combinedTOML.V2RegistriesConf}
|
||||
|
||||
// Post process registries, set the correct prefixes, sanity checks, etc.
|
||||
if err := c.postProcess(); err != nil {
|
||||
return err
|
||||
if err := res.partialV2.postProcessRegistries(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res.unqualifiedSearchRegistriesOrigin = path
|
||||
|
||||
if len(res.partialV2.ShortNameMode) > 0 {
|
||||
mode, err := parseShortNameMode(res.partialV2.ShortNameMode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
res.shortNameMode = mode
|
||||
} else {
|
||||
res.shortNameMode = types.ShortNameModeInvalid
|
||||
}
|
||||
|
||||
// Parse and validate short-name aliases.
|
||||
cache, err := newShortNameAliasCache(path, &res.partialV2.shortNameAliasConf)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error validating short-name aliases")
|
||||
}
|
||||
res.aliasCache = cache
|
||||
// Clear conf.partialV2.shortNameAliasConf to make it available for garbage collection and
|
||||
// reduce memory consumption. We're consulting aliasCache for lookups.
|
||||
res.partialV2.shortNameAliasConf = shortNameAliasConf{}
|
||||
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
// updateWithConfigurationFrom updates c with configuration from updates.
|
||||
//
|
||||
// Fields present in updates will typically replace already set fields in c.
|
||||
// The [[registry]] and alias tables are merged.
|
||||
func (c *parsedConfig) updateWithConfigurationFrom(updates *parsedConfig) {
|
||||
// == Merge Registries:
|
||||
registryMap := make(map[string]Registry)
|
||||
for i := range c.partialV2.Registries {
|
||||
registryMap[c.partialV2.Registries[i].Prefix] = c.partialV2.Registries[i]
|
||||
}
|
||||
// Merge the freshly loaded registries.
|
||||
for i := range c.Registries {
|
||||
registryMap[c.Registries[i].Prefix] = c.Registries[i]
|
||||
for i := range updates.partialV2.Registries {
|
||||
registryMap[updates.partialV2.Registries[i].Prefix] = updates.partialV2.Registries[i]
|
||||
}
|
||||
|
||||
// Go maps have a non-deterministic order when iterating the keys, so
|
||||
|
|
@ -686,10 +805,27 @@ func (c *tomlConfig) loadConfig(path string, forceV2 bool) error {
|
|||
}
|
||||
sort.Strings(prefixes)
|
||||
|
||||
c.Registries = []Registry{}
|
||||
c.partialV2.Registries = []Registry{}
|
||||
for _, prefix := range prefixes {
|
||||
c.Registries = append(c.Registries, registryMap[prefix])
|
||||
c.partialV2.Registries = append(c.partialV2.Registries, registryMap[prefix])
|
||||
}
|
||||
|
||||
return nil
|
||||
// == Merge UnqualifiedSearchRegistries:
|
||||
// This depends on an subtlety of the behavior of the TOML decoder, where a missing array field
|
||||
// is not modified while unmarshaling (in our case remains to nil), while an [] is unmarshaled
|
||||
// as a non-nil []string{}.
|
||||
if updates.partialV2.UnqualifiedSearchRegistries != nil {
|
||||
c.partialV2.UnqualifiedSearchRegistries = updates.partialV2.UnqualifiedSearchRegistries
|
||||
c.unqualifiedSearchRegistriesOrigin = updates.unqualifiedSearchRegistriesOrigin
|
||||
}
|
||||
|
||||
// == Merge shortNameMode:
|
||||
// We don’t maintain c.partialV2.ShortNameMode.
|
||||
if updates.shortNameMode != types.ShortNameModeInvalid {
|
||||
c.shortNameMode = updates.shortNameMode
|
||||
}
|
||||
|
||||
// == Merge aliasCache:
|
||||
// We don’t maintain (in fact we actively clear) c.partialV2.shortNameAliasConf.
|
||||
c.aliasCache.updateWithConfigurationFrom(updates.aliasCache)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -486,6 +486,36 @@ func NewOptionalBool(b bool) OptionalBool {
|
|||
return o
|
||||
}
|
||||
|
||||
// ShortNameMode defines the mode of short-name resolution.
|
||||
//
|
||||
// The use of unqualified-search registries entails an ambiguity as it's
|
||||
// unclear from which registry a given image, referenced by a short name, may
|
||||
// be pulled from.
|
||||
//
|
||||
// The ShortNameMode type defines how short names should resolve.
|
||||
type ShortNameMode int
|
||||
|
||||
const (
|
||||
ShortNameModeInvalid ShortNameMode = iota
|
||||
// Use all configured unqualified-search registries without prompting
|
||||
// the user.
|
||||
ShortNameModeDisabled
|
||||
// If stdout and stdin are a TTY, prompt the user to select a configured
|
||||
// unqualified-search registry. Otherwise, use all configured
|
||||
// unqualified-search registries.
|
||||
//
|
||||
// Note that if only one unqualified-search registry is set, it will be
|
||||
// used without prompting.
|
||||
ShortNameModePermissive
|
||||
// Always prompt the user to select a configured unqualified-search
|
||||
// registry. Throw an error if stdout or stdin is not a TTY as
|
||||
// prompting isn't possible.
|
||||
//
|
||||
// Note that if only one unqualified-search registry is set, it will be
|
||||
// used without prompting.
|
||||
ShortNameModeEnforcing
|
||||
)
|
||||
|
||||
// SystemContext allows parameterizing access to implicitly-accessed resources,
|
||||
// like configuration files in /etc and users' login state in their home directory.
|
||||
// Various components can share the same field only if their semantics is exactly
|
||||
|
|
@ -509,6 +539,10 @@ type SystemContext struct {
|
|||
SystemRegistriesConfPath string
|
||||
// Path to the system-wide registries configuration directory
|
||||
SystemRegistriesConfDirPath string
|
||||
// Path to the user-specific short-names configuration file
|
||||
UserShortNameAliasConfPath string
|
||||
// If set, short-name resolution in pkg/shortnames must follow the specified mode
|
||||
ShortNameMode *ShortNameMode
|
||||
// If not "", overrides the default path for the authentication file, but only new format files
|
||||
AuthFilePath string
|
||||
// if not "", overrides the default path for the authentication file, but with the legacy format;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ const (
|
|||
// VersionMajor is for an API incompatible changes
|
||||
VersionMajor = 5
|
||||
// VersionMinor is for functionality in a backwards-compatible manner
|
||||
VersionMinor = 7
|
||||
VersionMinor = 8
|
||||
// VersionPatch is for backwards-compatible bug fixes
|
||||
VersionPatch = 0
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,191 @@
|
|||
All files in this repository are licensed as follows. If you contribute
|
||||
to this repository, it is assumed that you license your contribution
|
||||
under the same license unless you state otherwise.
|
||||
|
||||
All files Copyright (C) 2015 Canonical Ltd. unless otherwise specified in the file.
|
||||
|
||||
This software is licensed under the LGPLv3, included below.
|
||||
|
||||
As a special exception to the GNU Lesser General Public License version 3
|
||||
("LGPL3"), the copyright holders of this Library give you permission to
|
||||
convey to a third party a Combined Work that links statically or dynamically
|
||||
to this Library without providing any Minimal Corresponding Source or
|
||||
Minimal Application Code as set out in 4d or providing the installation
|
||||
information set out in section 4e, provided that you comply with the other
|
||||
provisions of LGPL3 and provided that you meet, for the Application the
|
||||
terms and conditions of the license(s) which apply to the Application.
|
||||
|
||||
Except as stated in this special exception, the provisions of LGPL3 will
|
||||
continue to comply in full to this Library. If you modify this Library, you
|
||||
may apply this exception to your version of this Library, but you are not
|
||||
obliged to do so. If you do not wish to do so, delete this exception
|
||||
statement from your version. This exception does not (and cannot) modify any
|
||||
license terms which apply to the Application, with which you must still
|
||||
comply.
|
||||
|
||||
|
||||
GNU LESSER GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
|
||||
This version of the GNU Lesser General Public License incorporates
|
||||
the terms and conditions of version 3 of the GNU General Public
|
||||
License, supplemented by the additional permissions listed below.
|
||||
|
||||
0. Additional Definitions.
|
||||
|
||||
As used herein, "this License" refers to version 3 of the GNU Lesser
|
||||
General Public License, and the "GNU GPL" refers to version 3 of the GNU
|
||||
General Public License.
|
||||
|
||||
"The Library" refers to a covered work governed by this License,
|
||||
other than an Application or a Combined Work as defined below.
|
||||
|
||||
An "Application" is any work that makes use of an interface provided
|
||||
by the Library, but which is not otherwise based on the Library.
|
||||
Defining a subclass of a class defined by the Library is deemed a mode
|
||||
of using an interface provided by the Library.
|
||||
|
||||
A "Combined Work" is a work produced by combining or linking an
|
||||
Application with the Library. The particular version of the Library
|
||||
with which the Combined Work was made is also called the "Linked
|
||||
Version".
|
||||
|
||||
The "Minimal Corresponding Source" for a Combined Work means the
|
||||
Corresponding Source for the Combined Work, excluding any source code
|
||||
for portions of the Combined Work that, considered in isolation, are
|
||||
based on the Application, and not on the Linked Version.
|
||||
|
||||
The "Corresponding Application Code" for a Combined Work means the
|
||||
object code and/or source code for the Application, including any data
|
||||
and utility programs needed for reproducing the Combined Work from the
|
||||
Application, but excluding the System Libraries of the Combined Work.
|
||||
|
||||
1. Exception to Section 3 of the GNU GPL.
|
||||
|
||||
You may convey a covered work under sections 3 and 4 of this License
|
||||
without being bound by section 3 of the GNU GPL.
|
||||
|
||||
2. Conveying Modified Versions.
|
||||
|
||||
If you modify a copy of the Library, and, in your modifications, a
|
||||
facility refers to a function or data to be supplied by an Application
|
||||
that uses the facility (other than as an argument passed when the
|
||||
facility is invoked), then you may convey a copy of the modified
|
||||
version:
|
||||
|
||||
a) under this License, provided that you make a good faith effort to
|
||||
ensure that, in the event an Application does not supply the
|
||||
function or data, the facility still operates, and performs
|
||||
whatever part of its purpose remains meaningful, or
|
||||
|
||||
b) under the GNU GPL, with none of the additional permissions of
|
||||
this License applicable to that copy.
|
||||
|
||||
3. Object Code Incorporating Material from Library Header Files.
|
||||
|
||||
The object code form of an Application may incorporate material from
|
||||
a header file that is part of the Library. You may convey such object
|
||||
code under terms of your choice, provided that, if the incorporated
|
||||
material is not limited to numerical parameters, data structure
|
||||
layouts and accessors, or small macros, inline functions and templates
|
||||
(ten or fewer lines in length), you do both of the following:
|
||||
|
||||
a) Give prominent notice with each copy of the object code that the
|
||||
Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the object code with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
4. Combined Works.
|
||||
|
||||
You may convey a Combined Work under terms of your choice that,
|
||||
taken together, effectively do not restrict modification of the
|
||||
portions of the Library contained in the Combined Work and reverse
|
||||
engineering for debugging such modifications, if you also do each of
|
||||
the following:
|
||||
|
||||
a) Give prominent notice with each copy of the Combined Work that
|
||||
the Library is used in it and that the Library and its use are
|
||||
covered by this License.
|
||||
|
||||
b) Accompany the Combined Work with a copy of the GNU GPL and this license
|
||||
document.
|
||||
|
||||
c) For a Combined Work that displays copyright notices during
|
||||
execution, include the copyright notice for the Library among
|
||||
these notices, as well as a reference directing the user to the
|
||||
copies of the GNU GPL and this license document.
|
||||
|
||||
d) Do one of the following:
|
||||
|
||||
0) Convey the Minimal Corresponding Source under the terms of this
|
||||
License, and the Corresponding Application Code in a form
|
||||
suitable for, and under terms that permit, the user to
|
||||
recombine or relink the Application with a modified version of
|
||||
the Linked Version to produce a modified Combined Work, in the
|
||||
manner specified by section 6 of the GNU GPL for conveying
|
||||
Corresponding Source.
|
||||
|
||||
1) Use a suitable shared library mechanism for linking with the
|
||||
Library. A suitable mechanism is one that (a) uses at run time
|
||||
a copy of the Library already present on the user's computer
|
||||
system, and (b) will operate properly with a modified version
|
||||
of the Library that is interface-compatible with the Linked
|
||||
Version.
|
||||
|
||||
e) Provide Installation Information, but only if you would otherwise
|
||||
be required to provide such information under section 6 of the
|
||||
GNU GPL, and only to the extent that such information is
|
||||
necessary to install and execute a modified version of the
|
||||
Combined Work produced by recombining or relinking the
|
||||
Application with a modified version of the Linked Version. (If
|
||||
you use option 4d0, the Installation Information must accompany
|
||||
the Minimal Corresponding Source and Corresponding Application
|
||||
Code. If you use option 4d1, you must provide the Installation
|
||||
Information in the manner specified by section 6 of the GNU GPL
|
||||
for conveying Corresponding Source.)
|
||||
|
||||
5. Combined Libraries.
|
||||
|
||||
You may place library facilities that are a work based on the
|
||||
Library side by side in a single library together with other library
|
||||
facilities that are not Applications and are not covered by this
|
||||
License, and convey such a combined library under terms of your
|
||||
choice, if you do both of the following:
|
||||
|
||||
a) Accompany the combined library with a copy of the same work based
|
||||
on the Library, uncombined with any other library facilities,
|
||||
conveyed under the terms of this License.
|
||||
|
||||
b) Give prominent notice with the combined library that part of it
|
||||
is a work based on the Library, and explaining where to find the
|
||||
accompanying uncombined form of the same work.
|
||||
|
||||
6. Revised Versions of the GNU Lesser General Public License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions
|
||||
of the GNU Lesser General Public License from time to time. Such new
|
||||
versions will be similar in spirit to the present version, but may
|
||||
differ in detail to address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Library as you received it specifies that a certain numbered version
|
||||
of the GNU Lesser General Public License "or any later version"
|
||||
applies to it, you have the option of following the terms and
|
||||
conditions either of that published version or of any later version
|
||||
published by the Free Software Foundation. If the Library as you
|
||||
received it does not specify a version number of the GNU Lesser
|
||||
General Public License, you may choose any version of the GNU Lesser
|
||||
General Public License ever published by the Free Software Foundation.
|
||||
|
||||
If the Library as you received it specifies that a proxy can decide
|
||||
whether future versions of the GNU Lesser General Public License shall
|
||||
apply, that proxy's public statement of acceptance of any version is
|
||||
permanent authorization for you to choose that version for the
|
||||
Library.
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
# Copyright 2016 Canonical Ltd.
|
||||
# Licensed under the LGPLv3, see LICENCE file for details.
|
||||
|
||||
default: check
|
||||
|
||||
check:
|
||||
go test
|
||||
|
||||
docs:
|
||||
godoc2md github.com/juju/ansiterm > README.md
|
||||
sed -i 's|\[godoc-link-here\]|[](https://godoc.org/github.com/juju/ansiterm)|' README.md
|
||||
|
||||
|
||||
.PHONY: default check docs
|
||||
|
|
@ -0,0 +1,323 @@
|
|||
|
||||
# ansiterm
|
||||
import "github.com/juju/ansiterm"
|
||||
|
||||
Package ansiterm provides a Writer that writes out the ANSI escape
|
||||
codes for color and styles.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## type Color
|
||||
``` go
|
||||
type Color int
|
||||
```
|
||||
Color represents one of the standard 16 ANSI colors.
|
||||
|
||||
|
||||
|
||||
``` go
|
||||
const (
|
||||
Default Color
|
||||
Black
|
||||
Red
|
||||
Green
|
||||
Yellow
|
||||
Blue
|
||||
Magenta
|
||||
Cyan
|
||||
Gray
|
||||
DarkGray
|
||||
BrightRed
|
||||
BrightGreen
|
||||
BrightYellow
|
||||
BrightBlue
|
||||
BrightMagenta
|
||||
BrightCyan
|
||||
White
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### func (Color) String
|
||||
``` go
|
||||
func (c Color) String() string
|
||||
```
|
||||
String returns the name of the color.
|
||||
|
||||
|
||||
|
||||
## type Context
|
||||
``` go
|
||||
type Context struct {
|
||||
Foreground Color
|
||||
Background Color
|
||||
Styles []Style
|
||||
}
|
||||
```
|
||||
Context provides a way to specify both foreground and background colors
|
||||
along with other styles and write text to a Writer with those colors and
|
||||
styles.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### func Background
|
||||
``` go
|
||||
func Background(color Color) *Context
|
||||
```
|
||||
Background is a convenience function that creates a Context with the
|
||||
specified color as the background color.
|
||||
|
||||
|
||||
### func Foreground
|
||||
``` go
|
||||
func Foreground(color Color) *Context
|
||||
```
|
||||
Foreground is a convenience function that creates a Context with the
|
||||
specified color as the foreground color.
|
||||
|
||||
|
||||
### func Styles
|
||||
``` go
|
||||
func Styles(styles ...Style) *Context
|
||||
```
|
||||
Styles is a convenience function that creates a Context with the
|
||||
specified styles set.
|
||||
|
||||
|
||||
|
||||
|
||||
### func (\*Context) Fprint
|
||||
``` go
|
||||
func (c *Context) Fprint(w sgrWriter, args ...interface{})
|
||||
```
|
||||
Fprint will set the sgr values of the writer to the specified foreground,
|
||||
background and styles, then formats using the default formats for its
|
||||
operands and writes to w. Spaces are added between operands when neither is
|
||||
a string. It returns the number of bytes written and any write error
|
||||
encountered.
|
||||
|
||||
|
||||
|
||||
### func (\*Context) Fprintf
|
||||
``` go
|
||||
func (c *Context) Fprintf(w sgrWriter, format string, args ...interface{})
|
||||
```
|
||||
Fprintf will set the sgr values of the writer to the specified
|
||||
foreground, background and styles, then write the formatted string,
|
||||
then reset the writer.
|
||||
|
||||
|
||||
|
||||
### func (\*Context) SetBackground
|
||||
``` go
|
||||
func (c *Context) SetBackground(color Color) *Context
|
||||
```
|
||||
SetBackground sets the background to the specified color.
|
||||
|
||||
|
||||
|
||||
### func (\*Context) SetForeground
|
||||
``` go
|
||||
func (c *Context) SetForeground(color Color) *Context
|
||||
```
|
||||
SetForeground sets the foreground to the specified color.
|
||||
|
||||
|
||||
|
||||
### func (\*Context) SetStyle
|
||||
``` go
|
||||
func (c *Context) SetStyle(styles ...Style) *Context
|
||||
```
|
||||
SetStyle replaces the styles with the new values.
|
||||
|
||||
|
||||
|
||||
## type Style
|
||||
``` go
|
||||
type Style int
|
||||
```
|
||||
|
||||
|
||||
``` go
|
||||
const (
|
||||
Bold Style
|
||||
Faint
|
||||
Italic
|
||||
Underline
|
||||
Blink
|
||||
Reverse
|
||||
Strikethrough
|
||||
Conceal
|
||||
)
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### func (Style) String
|
||||
``` go
|
||||
func (s Style) String() string
|
||||
```
|
||||
|
||||
|
||||
## type TabWriter
|
||||
``` go
|
||||
type TabWriter struct {
|
||||
Writer
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
TabWriter is a filter that inserts padding around tab-delimited
|
||||
columns in its input to align them in the output.
|
||||
|
||||
It also setting of colors and styles over and above the standard
|
||||
tabwriter package.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### func NewTabWriter
|
||||
``` go
|
||||
func NewTabWriter(output io.Writer, minwidth, tabwidth, padding int, padchar byte, flags uint) *TabWriter
|
||||
```
|
||||
NewTabWriter returns a writer that is able to set colors and styels.
|
||||
The ansi escape codes are stripped for width calculations.
|
||||
|
||||
|
||||
|
||||
|
||||
### func (\*TabWriter) Flush
|
||||
``` go
|
||||
func (t *TabWriter) Flush() error
|
||||
```
|
||||
Flush should be called after the last call to Write to ensure
|
||||
that any data buffered in the Writer is written to output. Any
|
||||
incomplete escape sequence at the end is considered
|
||||
complete for formatting purposes.
|
||||
|
||||
|
||||
|
||||
### func (\*TabWriter) Init
|
||||
``` go
|
||||
func (t *TabWriter) Init(output io.Writer, minwidth, tabwidth, padding int, padchar byte, flags uint) *TabWriter
|
||||
```
|
||||
A Writer must be initialized with a call to Init. The first parameter (output)
|
||||
specifies the filter output. The remaining parameters control the formatting:
|
||||
|
||||
|
||||
minwidth minimal cell width including any padding
|
||||
tabwidth width of tab characters (equivalent number of spaces)
|
||||
padding padding added to a cell before computing its width
|
||||
padchar ASCII char used for padding
|
||||
if padchar == '\t', the Writer will assume that the
|
||||
width of a '\t' in the formatted output is tabwidth,
|
||||
and cells are left-aligned independent of align_left
|
||||
(for correct-looking results, tabwidth must correspond
|
||||
to the tab width in the viewer displaying the result)
|
||||
flags formatting control
|
||||
|
||||
|
||||
|
||||
## type Writer
|
||||
``` go
|
||||
type Writer struct {
|
||||
io.Writer
|
||||
// contains filtered or unexported fields
|
||||
}
|
||||
```
|
||||
Writer allows colors and styles to be specified. If the io.Writer
|
||||
is not a terminal capable of color, all attempts to set colors or
|
||||
styles are no-ops.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
### func NewWriter
|
||||
``` go
|
||||
func NewWriter(w io.Writer) *Writer
|
||||
```
|
||||
NewWriter returns a Writer that allows the caller to specify colors and
|
||||
styles. If the io.Writer is not a terminal capable of color, all attempts
|
||||
to set colors or styles are no-ops.
|
||||
|
||||
|
||||
|
||||
|
||||
### func (\*Writer) ClearStyle
|
||||
``` go
|
||||
func (w *Writer) ClearStyle(s Style)
|
||||
```
|
||||
ClearStyle clears the text style.
|
||||
|
||||
|
||||
|
||||
### func (\*Writer) Reset
|
||||
``` go
|
||||
func (w *Writer) Reset()
|
||||
```
|
||||
Reset returns the default foreground and background colors with no styles.
|
||||
|
||||
|
||||
|
||||
### func (\*Writer) SetBackground
|
||||
``` go
|
||||
func (w *Writer) SetBackground(c Color)
|
||||
```
|
||||
SetBackground sets the background color.
|
||||
|
||||
|
||||
|
||||
### func (\*Writer) SetForeground
|
||||
``` go
|
||||
func (w *Writer) SetForeground(c Color)
|
||||
```
|
||||
SetForeground sets the foreground color.
|
||||
|
||||
|
||||
|
||||
### func (\*Writer) SetStyle
|
||||
``` go
|
||||
func (w *Writer) SetStyle(s Style)
|
||||
```
|
||||
SetStyle sets the text style.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
- - -
|
||||
Generated by [godoc2md](http://godoc.org/github.com/davecheney/godoc2md)
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
// Copyright 2016 Canonical Ltd.
|
||||
// Licensed under the LGPLv3, see LICENCE file for details.
|
||||
|
||||
package ansiterm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type attribute int
|
||||
|
||||
const (
|
||||
unknownAttribute attribute = -1
|
||||
reset attribute = 0
|
||||
)
|
||||
|
||||
// sgr returns the escape sequence for the Select Graphic Rendition
|
||||
// for the attribute.
|
||||
func (a attribute) sgr() string {
|
||||
if a < 0 {
|
||||
return ""
|
||||
}
|
||||
return fmt.Sprintf("\x1b[%dm", a)
|
||||
}
|
||||
|
||||
type attributes []attribute
|
||||
|
||||
func (a attributes) Len() int { return len(a) }
|
||||
func (a attributes) Less(i, j int) bool { return a[i] < a[j] }
|
||||
func (a attributes) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
|
||||
|
||||
// sgr returns the combined escape sequence for the Select Graphic Rendition
|
||||
// for the sequence of attributes.
|
||||
func (a attributes) sgr() string {
|
||||
switch len(a) {
|
||||
case 0:
|
||||
return ""
|
||||
case 1:
|
||||
return a[0].sgr()
|
||||
default:
|
||||
sort.Sort(a)
|
||||
var values []string
|
||||
for _, attr := range a {
|
||||
values = append(values, fmt.Sprint(attr))
|
||||
}
|
||||
return fmt.Sprintf("\x1b[%sm", strings.Join(values, ";"))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
// Copyright 2016 Canonical Ltd.
|
||||
// Licensed under the LGPLv3, see LICENCE file for details.
|
||||
|
||||
package ansiterm
|
||||
|
||||
const (
|
||||
_ Color = iota
|
||||
Default
|
||||
Black
|
||||
Red
|
||||
Green
|
||||
Yellow
|
||||
Blue
|
||||
Magenta
|
||||
Cyan
|
||||
Gray
|
||||
DarkGray
|
||||
BrightRed
|
||||
BrightGreen
|
||||
BrightYellow
|
||||
BrightBlue
|
||||
BrightMagenta
|
||||
BrightCyan
|
||||
White
|
||||
)
|
||||
|
||||
// Color represents one of the standard 16 ANSI colors.
|
||||
type Color int
|
||||
|
||||
// String returns the name of the color.
|
||||
func (c Color) String() string {
|
||||
switch c {
|
||||
case Default:
|
||||
return "default"
|
||||
case Black:
|
||||
return "black"
|
||||
case Red:
|
||||
return "red"
|
||||
case Green:
|
||||
return "green"
|
||||
case Yellow:
|
||||
return "yellow"
|
||||
case Blue:
|
||||
return "blue"
|
||||
case Magenta:
|
||||
return "magenta"
|
||||
case Cyan:
|
||||
return "cyan"
|
||||
case Gray:
|
||||
return "gray"
|
||||
case DarkGray:
|
||||
return "darkgray"
|
||||
case BrightRed:
|
||||
return "brightred"
|
||||
case BrightGreen:
|
||||
return "brightgreen"
|
||||
case BrightYellow:
|
||||
return "brightyellow"
|
||||
case BrightBlue:
|
||||
return "brightblue"
|
||||
case BrightMagenta:
|
||||
return "brightmagenta"
|
||||
case BrightCyan:
|
||||
return "brightcyan"
|
||||
case White:
|
||||
return "white"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (c Color) foreground() attribute {
|
||||
switch c {
|
||||
case Default:
|
||||
return 39
|
||||
case Black:
|
||||
return 30
|
||||
case Red:
|
||||
return 31
|
||||
case Green:
|
||||
return 32
|
||||
case Yellow:
|
||||
return 33
|
||||
case Blue:
|
||||
return 34
|
||||
case Magenta:
|
||||
return 35
|
||||
case Cyan:
|
||||
return 36
|
||||
case Gray:
|
||||
return 37
|
||||
case DarkGray:
|
||||
return 90
|
||||
case BrightRed:
|
||||
return 91
|
||||
case BrightGreen:
|
||||
return 92
|
||||
case BrightYellow:
|
||||
return 93
|
||||
case BrightBlue:
|
||||
return 94
|
||||
case BrightMagenta:
|
||||
return 95
|
||||
case BrightCyan:
|
||||
return 96
|
||||
case White:
|
||||
return 97
|
||||
default:
|
||||
return unknownAttribute
|
||||
}
|
||||
}
|
||||
|
||||
func (c Color) background() attribute {
|
||||
value := c.foreground()
|
||||
if value != unknownAttribute {
|
||||
return value + 10
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
// Copyright 2016 Canonical Ltd.
|
||||
// Licensed under the LGPLv3, see LICENCE file for details.
|
||||
|
||||
package ansiterm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Context provides a way to specify both foreground and background colors
|
||||
// along with other styles and write text to a Writer with those colors and
|
||||
// styles.
|
||||
type Context struct {
|
||||
Foreground Color
|
||||
Background Color
|
||||
Styles []Style
|
||||
}
|
||||
|
||||
// Foreground is a convenience function that creates a Context with the
|
||||
// specified color as the foreground color.
|
||||
func Foreground(color Color) *Context {
|
||||
return &Context{Foreground: color}
|
||||
}
|
||||
|
||||
// Background is a convenience function that creates a Context with the
|
||||
// specified color as the background color.
|
||||
func Background(color Color) *Context {
|
||||
return &Context{Background: color}
|
||||
}
|
||||
|
||||
// Styles is a convenience function that creates a Context with the
|
||||
// specified styles set.
|
||||
func Styles(styles ...Style) *Context {
|
||||
return &Context{Styles: styles}
|
||||
}
|
||||
|
||||
// SetForeground sets the foreground to the specified color.
|
||||
func (c *Context) SetForeground(color Color) *Context {
|
||||
c.Foreground = color
|
||||
return c
|
||||
}
|
||||
|
||||
// SetBackground sets the background to the specified color.
|
||||
func (c *Context) SetBackground(color Color) *Context {
|
||||
c.Background = color
|
||||
return c
|
||||
}
|
||||
|
||||
// SetStyle replaces the styles with the new values.
|
||||
func (c *Context) SetStyle(styles ...Style) *Context {
|
||||
c.Styles = styles
|
||||
return c
|
||||
}
|
||||
|
||||
type sgrWriter interface {
|
||||
io.Writer
|
||||
writeSGR(value sgr)
|
||||
}
|
||||
|
||||
// Fprintf will set the sgr values of the writer to the specified
|
||||
// foreground, background and styles, then write the formatted string,
|
||||
// then reset the writer.
|
||||
func (c *Context) Fprintf(w sgrWriter, format string, args ...interface{}) {
|
||||
w.writeSGR(c)
|
||||
fmt.Fprintf(w, format, args...)
|
||||
w.writeSGR(reset)
|
||||
}
|
||||
|
||||
// Fprint will set the sgr values of the writer to the specified foreground,
|
||||
// background and styles, then formats using the default formats for its
|
||||
// operands and writes to w. Spaces are added between operands when neither is
|
||||
// a string. It returns the number of bytes written and any write error
|
||||
// encountered.
|
||||
func (c *Context) Fprint(w sgrWriter, args ...interface{}) {
|
||||
w.writeSGR(c)
|
||||
fmt.Fprint(w, args...)
|
||||
w.writeSGR(reset)
|
||||
}
|
||||
|
||||
func (c *Context) sgr() string {
|
||||
var values attributes
|
||||
if foreground := c.Foreground.foreground(); foreground != unknownAttribute {
|
||||
values = append(values, foreground)
|
||||
}
|
||||
if background := c.Background.background(); background != unknownAttribute {
|
||||
values = append(values, background)
|
||||
}
|
||||
for _, style := range c.Styles {
|
||||
if value := style.enable(); value != unknownAttribute {
|
||||
values = append(values, value)
|
||||
}
|
||||
}
|
||||
return values.sgr()
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright 2016 Canonical Ltd.
|
||||
// Licensed under the LGPLv3, see LICENCE file for details.
|
||||
|
||||
// Package ansiterm provides a Writer that writes out the ANSI escape
|
||||
// codes for color and styles.
|
||||
package ansiterm
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
// Copyright 2016 Canonical Ltd.
|
||||
// Licensed under the LGPLv3, see LICENCE file for details.
|
||||
|
||||
package ansiterm
|
||||
|
||||
const (
|
||||
_ Style = iota
|
||||
Bold
|
||||
Faint
|
||||
Italic
|
||||
Underline
|
||||
Blink
|
||||
Reverse
|
||||
Strikethrough
|
||||
Conceal
|
||||
)
|
||||
|
||||
type Style int
|
||||
|
||||
func (s Style) String() string {
|
||||
switch s {
|
||||
case Bold:
|
||||
return "bold"
|
||||
case Faint:
|
||||
return "faint"
|
||||
case Italic:
|
||||
return "italic"
|
||||
case Underline:
|
||||
return "underline"
|
||||
case Blink:
|
||||
return "blink"
|
||||
case Reverse:
|
||||
return "reverse"
|
||||
case Strikethrough:
|
||||
return "strikethrough"
|
||||
case Conceal:
|
||||
return "conceal"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func (s Style) enable() attribute {
|
||||
switch s {
|
||||
case Bold:
|
||||
return 1
|
||||
case Faint:
|
||||
return 2
|
||||
case Italic:
|
||||
return 3
|
||||
case Underline:
|
||||
return 4
|
||||
case Blink:
|
||||
return 5
|
||||
case Reverse:
|
||||
return 7
|
||||
case Conceal:
|
||||
return 8
|
||||
case Strikethrough:
|
||||
return 9
|
||||
default:
|
||||
return unknownAttribute
|
||||
}
|
||||
}
|
||||
|
||||
func (s Style) disable() attribute {
|
||||
value := s.enable()
|
||||
if value != unknownAttribute {
|
||||
return value + 20
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
// Copyright 2016 Canonical Ltd.
|
||||
// Licensed under the LGPLv3, see LICENCE file for details.
|
||||
|
||||
package ansiterm
|
||||
|
||||
import (
|
||||
"io"
|
||||
|
||||
"github.com/juju/ansiterm/tabwriter"
|
||||
)
|
||||
|
||||
// NewTabWriter returns a writer that is able to set colors and styels.
|
||||
// The ansi escape codes are stripped for width calculations.
|
||||
func NewTabWriter(output io.Writer, minwidth, tabwidth, padding int, padchar byte, flags uint) *TabWriter {
|
||||
return new(TabWriter).Init(output, minwidth, tabwidth, padding, padchar, flags)
|
||||
}
|
||||
|
||||
// TabWriter is a filter that inserts padding around tab-delimited
|
||||
// columns in its input to align them in the output.
|
||||
//
|
||||
// It also setting of colors and styles over and above the standard
|
||||
// tabwriter package.
|
||||
type TabWriter struct {
|
||||
Writer
|
||||
tw tabwriter.Writer
|
||||
}
|
||||
|
||||
// Flush should be called after the last call to Write to ensure
|
||||
// that any data buffered in the Writer is written to output. Any
|
||||
// incomplete escape sequence at the end is considered
|
||||
// complete for formatting purposes.
|
||||
//
|
||||
func (t *TabWriter) Flush() error {
|
||||
return t.tw.Flush()
|
||||
}
|
||||
|
||||
// SetColumnAlignRight will mark a particular column as align right.
|
||||
// This is reset on the next flush.
|
||||
func (t *TabWriter) SetColumnAlignRight(column int) {
|
||||
t.tw.SetColumnAlignRight(column)
|
||||
}
|
||||
|
||||
// A Writer must be initialized with a call to Init. The first parameter (output)
|
||||
// specifies the filter output. The remaining parameters control the formatting:
|
||||
//
|
||||
// minwidth minimal cell width including any padding
|
||||
// tabwidth width of tab characters (equivalent number of spaces)
|
||||
// padding padding added to a cell before computing its width
|
||||
// padchar ASCII char used for padding
|
||||
// if padchar == '\t', the Writer will assume that the
|
||||
// width of a '\t' in the formatted output is tabwidth,
|
||||
// and cells are left-aligned independent of align_left
|
||||
// (for correct-looking results, tabwidth must correspond
|
||||
// to the tab width in the viewer displaying the result)
|
||||
// flags formatting control
|
||||
//
|
||||
func (t *TabWriter) Init(output io.Writer, minwidth, tabwidth, padding int, padchar byte, flags uint) *TabWriter {
|
||||
writer, colorCapable := colorEnabledWriter(output)
|
||||
t.Writer = Writer{
|
||||
Writer: t.tw.Init(writer, minwidth, tabwidth, padding, padchar, flags),
|
||||
noColor: !colorCapable,
|
||||
}
|
||||
return t
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
Copyright (c) 2012 The Go Authors. All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are
|
||||
met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright
|
||||
notice, this list of conditions and the following disclaimer.
|
||||
* Redistributions in binary form must reproduce the above
|
||||
copyright notice, this list of conditions and the following disclaimer
|
||||
in the documentation and/or other materials provided with the
|
||||
distribution.
|
||||
* Neither the name of Google Inc. nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
|
||||
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
|
||||
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
|
||||
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
|
||||
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
|
||||
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
|
||||
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
||||
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
|
||||
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
|
@ -0,0 +1,587 @@
|
|||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This file is mostly a copy of the go standard library text/tabwriter. With
|
||||
// the additional stripping of ansi control characters for width calculations.
|
||||
|
||||
// Package tabwriter implements a write filter (tabwriter.Writer) that
|
||||
// translates tabbed columns in input into properly aligned text.
|
||||
//
|
||||
// The package is using the Elastic Tabstops algorithm described at
|
||||
// http://nickgravgaard.com/elastictabstops/index.html.
|
||||
//
|
||||
package tabwriter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/lunixbochs/vtclean"
|
||||
)
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Filter implementation
|
||||
|
||||
// A cell represents a segment of text terminated by tabs or line breaks.
|
||||
// The text itself is stored in a separate buffer; cell only describes the
|
||||
// segment's size in bytes, its width in runes, and whether it's an htab
|
||||
// ('\t') terminated cell.
|
||||
//
|
||||
type cell struct {
|
||||
size int // cell size in bytes
|
||||
width int // cell width in runes
|
||||
htab bool // true if the cell is terminated by an htab ('\t')
|
||||
}
|
||||
|
||||
// A Writer is a filter that inserts padding around tab-delimited
|
||||
// columns in its input to align them in the output.
|
||||
//
|
||||
// The Writer treats incoming bytes as UTF-8 encoded text consisting
|
||||
// of cells terminated by (horizontal or vertical) tabs or line
|
||||
// breaks (newline or formfeed characters). Cells in adjacent lines
|
||||
// constitute a column. The Writer inserts padding as needed to
|
||||
// make all cells in a column have the same width, effectively
|
||||
// aligning the columns. It assumes that all characters have the
|
||||
// same width except for tabs for which a tabwidth must be specified.
|
||||
// Note that cells are tab-terminated, not tab-separated: trailing
|
||||
// non-tab text at the end of a line does not form a column cell.
|
||||
//
|
||||
// The Writer assumes that all Unicode code points have the same width;
|
||||
// this may not be true in some fonts.
|
||||
//
|
||||
// If DiscardEmptyColumns is set, empty columns that are terminated
|
||||
// entirely by vertical (or "soft") tabs are discarded. Columns
|
||||
// terminated by horizontal (or "hard") tabs are not affected by
|
||||
// this flag.
|
||||
//
|
||||
// If a Writer is configured to filter HTML, HTML tags and entities
|
||||
// are passed through. The widths of tags and entities are
|
||||
// assumed to be zero (tags) and one (entities) for formatting purposes.
|
||||
//
|
||||
// A segment of text may be escaped by bracketing it with Escape
|
||||
// characters. The tabwriter passes escaped text segments through
|
||||
// unchanged. In particular, it does not interpret any tabs or line
|
||||
// breaks within the segment. If the StripEscape flag is set, the
|
||||
// Escape characters are stripped from the output; otherwise they
|
||||
// are passed through as well. For the purpose of formatting, the
|
||||
// width of the escaped text is always computed excluding the Escape
|
||||
// characters.
|
||||
//
|
||||
// The formfeed character ('\f') acts like a newline but it also
|
||||
// terminates all columns in the current line (effectively calling
|
||||
// Flush). Cells in the next line start new columns. Unless found
|
||||
// inside an HTML tag or inside an escaped text segment, formfeed
|
||||
// characters appear as newlines in the output.
|
||||
//
|
||||
// The Writer must buffer input internally, because proper spacing
|
||||
// of one line may depend on the cells in future lines. Clients must
|
||||
// call Flush when done calling Write.
|
||||
//
|
||||
type Writer struct {
|
||||
// configuration
|
||||
output io.Writer
|
||||
minwidth int
|
||||
tabwidth int
|
||||
padding int
|
||||
padbytes [8]byte
|
||||
flags uint
|
||||
|
||||
// current state
|
||||
buf bytes.Buffer // collected text excluding tabs or line breaks
|
||||
pos int // buffer position up to which cell.width of incomplete cell has been computed
|
||||
cell cell // current incomplete cell; cell.width is up to buf[pos] excluding ignored sections
|
||||
endChar byte // terminating char of escaped sequence (Escape for escapes, '>', ';' for HTML tags/entities, or 0)
|
||||
lines [][]cell // list of lines; each line is a list of cells
|
||||
widths []int // list of column widths in runes - re-used during formatting
|
||||
alignment map[int]uint // column alignment
|
||||
}
|
||||
|
||||
func (b *Writer) addLine() { b.lines = append(b.lines, []cell{}) }
|
||||
|
||||
// Reset the current state.
|
||||
func (b *Writer) reset() {
|
||||
b.buf.Reset()
|
||||
b.pos = 0
|
||||
b.cell = cell{}
|
||||
b.endChar = 0
|
||||
b.lines = b.lines[0:0]
|
||||
b.widths = b.widths[0:0]
|
||||
b.alignment = make(map[int]uint)
|
||||
b.addLine()
|
||||
}
|
||||
|
||||
// Internal representation (current state):
|
||||
//
|
||||
// - all text written is appended to buf; tabs and line breaks are stripped away
|
||||
// - at any given time there is a (possibly empty) incomplete cell at the end
|
||||
// (the cell starts after a tab or line break)
|
||||
// - cell.size is the number of bytes belonging to the cell so far
|
||||
// - cell.width is text width in runes of that cell from the start of the cell to
|
||||
// position pos; html tags and entities are excluded from this width if html
|
||||
// filtering is enabled
|
||||
// - the sizes and widths of processed text are kept in the lines list
|
||||
// which contains a list of cells for each line
|
||||
// - the widths list is a temporary list with current widths used during
|
||||
// formatting; it is kept in Writer because it's re-used
|
||||
//
|
||||
// |<---------- size ---------->|
|
||||
// | |
|
||||
// |<- width ->|<- ignored ->| |
|
||||
// | | | |
|
||||
// [---processed---tab------------<tag>...</tag>...]
|
||||
// ^ ^ ^
|
||||
// | | |
|
||||
// buf start of incomplete cell pos
|
||||
|
||||
// Formatting can be controlled with these flags.
|
||||
const (
|
||||
// Ignore html tags and treat entities (starting with '&'
|
||||
// and ending in ';') as single characters (width = 1).
|
||||
FilterHTML uint = 1 << iota
|
||||
|
||||
// Strip Escape characters bracketing escaped text segments
|
||||
// instead of passing them through unchanged with the text.
|
||||
StripEscape
|
||||
|
||||
// Force right-alignment of cell content.
|
||||
// Default is left-alignment.
|
||||
AlignRight
|
||||
|
||||
// Handle empty columns as if they were not present in
|
||||
// the input in the first place.
|
||||
DiscardEmptyColumns
|
||||
|
||||
// Always use tabs for indentation columns (i.e., padding of
|
||||
// leading empty cells on the left) independent of padchar.
|
||||
TabIndent
|
||||
|
||||
// Print a vertical bar ('|') between columns (after formatting).
|
||||
// Discarded columns appear as zero-width columns ("||").
|
||||
Debug
|
||||
)
|
||||
|
||||
// A Writer must be initialized with a call to Init. The first parameter (output)
|
||||
// specifies the filter output. The remaining parameters control the formatting:
|
||||
//
|
||||
// minwidth minimal cell width including any padding
|
||||
// tabwidth width of tab characters (equivalent number of spaces)
|
||||
// padding padding added to a cell before computing its width
|
||||
// padchar ASCII char used for padding
|
||||
// if padchar == '\t', the Writer will assume that the
|
||||
// width of a '\t' in the formatted output is tabwidth,
|
||||
// and cells are left-aligned independent of align_left
|
||||
// (for correct-looking results, tabwidth must correspond
|
||||
// to the tab width in the viewer displaying the result)
|
||||
// flags formatting control
|
||||
//
|
||||
func (b *Writer) Init(output io.Writer, minwidth, tabwidth, padding int, padchar byte, flags uint) *Writer {
|
||||
if minwidth < 0 || tabwidth < 0 || padding < 0 {
|
||||
panic("negative minwidth, tabwidth, or padding")
|
||||
}
|
||||
b.output = output
|
||||
b.minwidth = minwidth
|
||||
b.tabwidth = tabwidth
|
||||
b.padding = padding
|
||||
for i := range b.padbytes {
|
||||
b.padbytes[i] = padchar
|
||||
}
|
||||
if padchar == '\t' {
|
||||
// tab padding enforces left-alignment
|
||||
flags &^= AlignRight
|
||||
}
|
||||
b.flags = flags
|
||||
|
||||
b.reset()
|
||||
|
||||
return b
|
||||
}
|
||||
|
||||
// debugging support (keep code around)
|
||||
func (b *Writer) dump() {
|
||||
pos := 0
|
||||
for i, line := range b.lines {
|
||||
print("(", i, ") ")
|
||||
for _, c := range line {
|
||||
print("[", string(b.buf.Bytes()[pos:pos+c.size]), "]")
|
||||
pos += c.size
|
||||
}
|
||||
print("\n")
|
||||
}
|
||||
print("\n")
|
||||
}
|
||||
|
||||
// local error wrapper so we can distinguish errors we want to return
|
||||
// as errors from genuine panics (which we don't want to return as errors)
|
||||
type osError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (b *Writer) write0(buf []byte) {
|
||||
n, err := b.output.Write(buf)
|
||||
if n != len(buf) && err == nil {
|
||||
err = io.ErrShortWrite
|
||||
}
|
||||
if err != nil {
|
||||
panic(osError{err})
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Writer) writeN(src []byte, n int) {
|
||||
for n > len(src) {
|
||||
b.write0(src)
|
||||
n -= len(src)
|
||||
}
|
||||
b.write0(src[0:n])
|
||||
}
|
||||
|
||||
var (
|
||||
newline = []byte{'\n'}
|
||||
tabs = []byte("\t\t\t\t\t\t\t\t")
|
||||
)
|
||||
|
||||
func (b *Writer) writePadding(textw, cellw int, useTabs bool) {
|
||||
if b.padbytes[0] == '\t' || useTabs {
|
||||
// padding is done with tabs
|
||||
if b.tabwidth == 0 {
|
||||
return // tabs have no width - can't do any padding
|
||||
}
|
||||
// make cellw the smallest multiple of b.tabwidth
|
||||
cellw = (cellw + b.tabwidth - 1) / b.tabwidth * b.tabwidth
|
||||
n := cellw - textw // amount of padding
|
||||
if n < 0 {
|
||||
panic("internal error")
|
||||
}
|
||||
b.writeN(tabs, (n+b.tabwidth-1)/b.tabwidth)
|
||||
return
|
||||
}
|
||||
|
||||
// padding is done with non-tab characters
|
||||
b.writeN(b.padbytes[0:], cellw-textw)
|
||||
}
|
||||
|
||||
var vbar = []byte{'|'}
|
||||
|
||||
func (b *Writer) writeLines(pos0 int, line0, line1 int) (pos int) {
|
||||
pos = pos0
|
||||
for i := line0; i < line1; i++ {
|
||||
line := b.lines[i]
|
||||
|
||||
// if TabIndent is set, use tabs to pad leading empty cells
|
||||
useTabs := b.flags&TabIndent != 0
|
||||
|
||||
for j, c := range line {
|
||||
if j > 0 && b.flags&Debug != 0 {
|
||||
// indicate column break
|
||||
b.write0(vbar)
|
||||
}
|
||||
|
||||
if c.size == 0 {
|
||||
// empty cell
|
||||
if j < len(b.widths) {
|
||||
b.writePadding(c.width, b.widths[j], useTabs)
|
||||
}
|
||||
} else {
|
||||
// non-empty cell
|
||||
useTabs = false
|
||||
alignColumnRight := b.alignment[j] == AlignRight
|
||||
if (b.flags&AlignRight == 0) && !alignColumnRight { // align left
|
||||
b.write0(b.buf.Bytes()[pos : pos+c.size])
|
||||
pos += c.size
|
||||
if j < len(b.widths) {
|
||||
b.writePadding(c.width, b.widths[j], false)
|
||||
}
|
||||
} else if alignColumnRight && j < len(b.widths) {
|
||||
// just this column
|
||||
internalSize := b.widths[j] - b.padding
|
||||
if j < len(b.widths) {
|
||||
b.writePadding(c.width, internalSize, false)
|
||||
}
|
||||
b.write0(b.buf.Bytes()[pos : pos+c.size])
|
||||
if b.padding > 0 {
|
||||
b.writePadding(0, b.padding, false)
|
||||
}
|
||||
pos += c.size
|
||||
} else { // align right
|
||||
if j < len(b.widths) {
|
||||
b.writePadding(c.width, b.widths[j], false)
|
||||
}
|
||||
b.write0(b.buf.Bytes()[pos : pos+c.size])
|
||||
pos += c.size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if i+1 == len(b.lines) {
|
||||
// last buffered line - we don't have a newline, so just write
|
||||
// any outstanding buffered data
|
||||
b.write0(b.buf.Bytes()[pos : pos+b.cell.size])
|
||||
pos += b.cell.size
|
||||
} else {
|
||||
// not the last line - write newline
|
||||
b.write0(newline)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Format the text between line0 and line1 (excluding line1); pos
|
||||
// is the buffer position corresponding to the beginning of line0.
|
||||
// Returns the buffer position corresponding to the beginning of
|
||||
// line1 and an error, if any.
|
||||
//
|
||||
func (b *Writer) format(pos0 int, line0, line1 int) (pos int) {
|
||||
pos = pos0
|
||||
column := len(b.widths)
|
||||
for this := line0; this < line1; this++ {
|
||||
line := b.lines[this]
|
||||
|
||||
if column < len(line)-1 {
|
||||
// cell exists in this column => this line
|
||||
// has more cells than the previous line
|
||||
// (the last cell per line is ignored because cells are
|
||||
// tab-terminated; the last cell per line describes the
|
||||
// text before the newline/formfeed and does not belong
|
||||
// to a column)
|
||||
|
||||
// print unprinted lines until beginning of block
|
||||
pos = b.writeLines(pos, line0, this)
|
||||
line0 = this
|
||||
|
||||
// column block begin
|
||||
width := b.minwidth // minimal column width
|
||||
discardable := true // true if all cells in this column are empty and "soft"
|
||||
for ; this < line1; this++ {
|
||||
line = b.lines[this]
|
||||
if column < len(line)-1 {
|
||||
// cell exists in this column
|
||||
c := line[column]
|
||||
// update width
|
||||
if w := c.width + b.padding; w > width {
|
||||
width = w
|
||||
}
|
||||
// update discardable
|
||||
if c.width > 0 || c.htab {
|
||||
discardable = false
|
||||
}
|
||||
} else {
|
||||
break
|
||||
}
|
||||
}
|
||||
// column block end
|
||||
|
||||
// discard empty columns if necessary
|
||||
if discardable && b.flags&DiscardEmptyColumns != 0 {
|
||||
width = 0
|
||||
}
|
||||
|
||||
// format and print all columns to the right of this column
|
||||
// (we know the widths of this column and all columns to the left)
|
||||
b.widths = append(b.widths, width) // push width
|
||||
pos = b.format(pos, line0, this)
|
||||
b.widths = b.widths[0 : len(b.widths)-1] // pop width
|
||||
line0 = this
|
||||
}
|
||||
}
|
||||
|
||||
// print unprinted lines until end
|
||||
return b.writeLines(pos, line0, line1)
|
||||
}
|
||||
|
||||
// Append text to current cell.
|
||||
func (b *Writer) append(text []byte) {
|
||||
b.buf.Write(text)
|
||||
b.cell.size += len(text)
|
||||
}
|
||||
|
||||
// Update the cell width.
|
||||
func (b *Writer) updateWidth() {
|
||||
// ---- Changes here -----
|
||||
newChars := b.buf.Bytes()[b.pos:b.buf.Len()]
|
||||
cleaned := vtclean.Clean(string(newChars), false) // false to strip colors
|
||||
b.cell.width += utf8.RuneCount([]byte(cleaned))
|
||||
// --- end of changes ----
|
||||
b.pos = b.buf.Len()
|
||||
}
|
||||
|
||||
// To escape a text segment, bracket it with Escape characters.
|
||||
// For instance, the tab in this string "Ignore this tab: \xff\t\xff"
|
||||
// does not terminate a cell and constitutes a single character of
|
||||
// width one for formatting purposes.
|
||||
//
|
||||
// The value 0xff was chosen because it cannot appear in a valid UTF-8 sequence.
|
||||
//
|
||||
const Escape = '\xff'
|
||||
|
||||
// Start escaped mode.
|
||||
func (b *Writer) startEscape(ch byte) {
|
||||
switch ch {
|
||||
case Escape:
|
||||
b.endChar = Escape
|
||||
case '<':
|
||||
b.endChar = '>'
|
||||
case '&':
|
||||
b.endChar = ';'
|
||||
}
|
||||
}
|
||||
|
||||
// Terminate escaped mode. If the escaped text was an HTML tag, its width
|
||||
// is assumed to be zero for formatting purposes; if it was an HTML entity,
|
||||
// its width is assumed to be one. In all other cases, the width is the
|
||||
// unicode width of the text.
|
||||
//
|
||||
func (b *Writer) endEscape() {
|
||||
switch b.endChar {
|
||||
case Escape:
|
||||
b.updateWidth()
|
||||
if b.flags&StripEscape == 0 {
|
||||
b.cell.width -= 2 // don't count the Escape chars
|
||||
}
|
||||
case '>': // tag of zero width
|
||||
case ';':
|
||||
b.cell.width++ // entity, count as one rune
|
||||
}
|
||||
b.pos = b.buf.Len()
|
||||
b.endChar = 0
|
||||
}
|
||||
|
||||
// Terminate the current cell by adding it to the list of cells of the
|
||||
// current line. Returns the number of cells in that line.
|
||||
//
|
||||
func (b *Writer) terminateCell(htab bool) int {
|
||||
b.cell.htab = htab
|
||||
line := &b.lines[len(b.lines)-1]
|
||||
*line = append(*line, b.cell)
|
||||
b.cell = cell{}
|
||||
return len(*line)
|
||||
}
|
||||
|
||||
func handlePanic(err *error, op string) {
|
||||
if e := recover(); e != nil {
|
||||
if nerr, ok := e.(osError); ok {
|
||||
*err = nerr.err
|
||||
return
|
||||
}
|
||||
panic("tabwriter: panic during " + op)
|
||||
}
|
||||
}
|
||||
|
||||
// Flush should be called after the last call to Write to ensure
|
||||
// that any data buffered in the Writer is written to output. Any
|
||||
// incomplete escape sequence at the end is considered
|
||||
// complete for formatting purposes.
|
||||
//
|
||||
func (b *Writer) Flush() (err error) {
|
||||
defer b.reset() // even in the presence of errors
|
||||
defer handlePanic(&err, "Flush")
|
||||
|
||||
// add current cell if not empty
|
||||
if b.cell.size > 0 {
|
||||
if b.endChar != 0 {
|
||||
// inside escape - terminate it even if incomplete
|
||||
b.endEscape()
|
||||
}
|
||||
b.terminateCell(false)
|
||||
}
|
||||
|
||||
// format contents of buffer
|
||||
b.format(0, 0, len(b.lines))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var hbar = []byte("---\n")
|
||||
|
||||
// SetColumnAlignRight will mark a particular column as align right.
|
||||
// This is reset on the next flush.
|
||||
func (b *Writer) SetColumnAlignRight(column int) {
|
||||
b.alignment[column] = AlignRight
|
||||
}
|
||||
|
||||
// Write writes buf to the writer b.
|
||||
// The only errors returned are ones encountered
|
||||
// while writing to the underlying output stream.
|
||||
//
|
||||
func (b *Writer) Write(buf []byte) (n int, err error) {
|
||||
defer handlePanic(&err, "Write")
|
||||
|
||||
// split text into cells
|
||||
n = 0
|
||||
for i, ch := range buf {
|
||||
if b.endChar == 0 {
|
||||
// outside escape
|
||||
switch ch {
|
||||
case '\t', '\v', '\n', '\f':
|
||||
// end of cell
|
||||
b.append(buf[n:i])
|
||||
b.updateWidth()
|
||||
n = i + 1 // ch consumed
|
||||
ncells := b.terminateCell(ch == '\t')
|
||||
if ch == '\n' || ch == '\f' {
|
||||
// terminate line
|
||||
b.addLine()
|
||||
if ch == '\f' || ncells == 1 {
|
||||
// A '\f' always forces a flush. Otherwise, if the previous
|
||||
// line has only one cell which does not have an impact on
|
||||
// the formatting of the following lines (the last cell per
|
||||
// line is ignored by format()), thus we can flush the
|
||||
// Writer contents.
|
||||
if err = b.Flush(); err != nil {
|
||||
return
|
||||
}
|
||||
if ch == '\f' && b.flags&Debug != 0 {
|
||||
// indicate section break
|
||||
b.write0(hbar)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case Escape:
|
||||
// start of escaped sequence
|
||||
b.append(buf[n:i])
|
||||
b.updateWidth()
|
||||
n = i
|
||||
if b.flags&StripEscape != 0 {
|
||||
n++ // strip Escape
|
||||
}
|
||||
b.startEscape(Escape)
|
||||
|
||||
case '<', '&':
|
||||
// possibly an html tag/entity
|
||||
if b.flags&FilterHTML != 0 {
|
||||
// begin of tag/entity
|
||||
b.append(buf[n:i])
|
||||
b.updateWidth()
|
||||
n = i
|
||||
b.startEscape(ch)
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// inside escape
|
||||
if ch == b.endChar {
|
||||
// end of tag/entity
|
||||
j := i + 1
|
||||
if ch == Escape && b.flags&StripEscape != 0 {
|
||||
j = i // strip Escape
|
||||
}
|
||||
b.append(buf[n:j])
|
||||
n = i + 1 // ch consumed
|
||||
b.endEscape()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// append leftover text
|
||||
b.append(buf[n:])
|
||||
n = len(buf)
|
||||
return
|
||||
}
|
||||
|
||||
// NewWriter allocates and initializes a new tabwriter.Writer.
|
||||
// The parameters are the same as for the Init function.
|
||||
//
|
||||
func NewWriter(output io.Writer, minwidth, tabwidth, padding int, padchar byte, flags uint) *Writer {
|
||||
return new(Writer).Init(output, minwidth, tabwidth, padding, padchar, flags)
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
// Copyright 2016 Canonical Ltd.
|
||||
// Licensed under the LGPLv3, see LICENCE file for details.
|
||||
|
||||
package ansiterm
|
||||
|
||||
import (
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"github.com/mattn/go-colorable"
|
||||
"github.com/mattn/go-isatty"
|
||||
)
|
||||
|
||||
// colorEnabledWriter returns a writer that can handle the ansi color codes
|
||||
// and true if the writer passed in is a terminal capable of color. If the
|
||||
// TERM environment variable is set to "dumb", the terminal is not considered
|
||||
// color capable.
|
||||
func colorEnabledWriter(w io.Writer) (io.Writer, bool) {
|
||||
f, ok := w.(*os.File)
|
||||
if !ok {
|
||||
return w, false
|
||||
}
|
||||
// Check the TERM environment variable specifically
|
||||
// to check for "dumb" terminals.
|
||||
if os.Getenv("TERM") == "dumb" {
|
||||
return w, false
|
||||
}
|
||||
if !isatty.IsTerminal(f.Fd()) {
|
||||
return w, false
|
||||
}
|
||||
return colorable.NewColorable(f), true
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
// Copyright 2016 Canonical Ltd.
|
||||
// Licensed under the LGPLv3, see LICENCE file for details.
|
||||
|
||||
package ansiterm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// Writer allows colors and styles to be specified. If the io.Writer
|
||||
// is not a terminal capable of color, all attempts to set colors or
|
||||
// styles are no-ops.
|
||||
type Writer struct {
|
||||
io.Writer
|
||||
|
||||
noColor bool
|
||||
}
|
||||
|
||||
// NewWriter returns a Writer that allows the caller to specify colors and
|
||||
// styles. If the io.Writer is not a terminal capable of color, all attempts
|
||||
// to set colors or styles are no-ops.
|
||||
func NewWriter(w io.Writer) *Writer {
|
||||
writer, colorCapable := colorEnabledWriter(w)
|
||||
return &Writer{
|
||||
Writer: writer,
|
||||
noColor: !colorCapable,
|
||||
}
|
||||
}
|
||||
|
||||
// SetColorCapable forces the writer to either write the ANSI escape color
|
||||
// if capable is true, or to not write them if capable is false.
|
||||
func (w *Writer) SetColorCapable(capable bool) {
|
||||
w.noColor = !capable
|
||||
}
|
||||
|
||||
// SetForeground sets the foreground color.
|
||||
func (w *Writer) SetForeground(c Color) {
|
||||
w.writeSGR(c.foreground())
|
||||
}
|
||||
|
||||
// SetBackground sets the background color.
|
||||
func (w *Writer) SetBackground(c Color) {
|
||||
w.writeSGR(c.background())
|
||||
}
|
||||
|
||||
// SetStyle sets the text style.
|
||||
func (w *Writer) SetStyle(s Style) {
|
||||
w.writeSGR(s.enable())
|
||||
}
|
||||
|
||||
// ClearStyle clears the text style.
|
||||
func (w *Writer) ClearStyle(s Style) {
|
||||
w.writeSGR(s.disable())
|
||||
}
|
||||
|
||||
// Reset returns the default foreground and background colors with no styles.
|
||||
func (w *Writer) Reset() {
|
||||
w.writeSGR(reset)
|
||||
}
|
||||
|
||||
type sgr interface {
|
||||
// sgr returns the combined escape sequence for the Select Graphic Rendition.
|
||||
sgr() string
|
||||
}
|
||||
|
||||
// writeSGR takes the appropriate integer SGR parameters
|
||||
// and writes out the ANIS escape code.
|
||||
func (w *Writer) writeSGR(value sgr) {
|
||||
if w.noColor {
|
||||
return
|
||||
}
|
||||
fmt.Fprint(w, value.sgr())
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
language: go
|
||||
sudo: false
|
||||
|
||||
script: go test -v
|
||||
|
||||
go:
|
||||
- 1.5
|
||||
- 1.6
|
||||
- 1.7
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
Copyright (c) 2015 Ryan Hileman
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
[](https://travis-ci.org/lunixbochs/vtclean)
|
||||
|
||||
vtclean
|
||||
----
|
||||
|
||||
Clean up raw terminal output by stripping escape sequences, optionally preserving color.
|
||||
|
||||
Get it: `go get github.com/lunixbochs/vtclean/vtclean`
|
||||
|
||||
API:
|
||||
|
||||
import "github.com/lunixbochs/vtclean"
|
||||
vtclean.Clean(line string, color bool) string
|
||||
|
||||
Command line example:
|
||||
|
||||
$ echo -e '\x1b[1;32mcolor example
|
||||
color forced to stop at end of line
|
||||
backspace is ba\b\bgood
|
||||
no beeps!\x07\x07' | ./vtclean -color
|
||||
|
||||
color example
|
||||
color forced to stop at end of line
|
||||
backspace is good
|
||||
no beeps!
|
||||
|
||||
Go example:
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/lunixbochs/vtclean"
|
||||
)
|
||||
|
||||
func main() {
|
||||
line := vtclean.Clean(
|
||||
"\033[1;32mcolor, " +
|
||||
"curs\033[Aor, " +
|
||||
"backspace\b\b\b\b\b\b\b\b\b\b\b\033[K", false)
|
||||
fmt.Println(line)
|
||||
}
|
||||
|
||||
Output:
|
||||
|
||||
color, cursor
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package vtclean
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"io"
|
||||
)
|
||||
|
||||
type reader struct {
|
||||
io.Reader
|
||||
scanner *bufio.Scanner
|
||||
buf []byte
|
||||
|
||||
color bool
|
||||
}
|
||||
|
||||
func NewReader(r io.Reader, color bool) io.Reader {
|
||||
return &reader{Reader: r, color: color}
|
||||
}
|
||||
|
||||
func (r *reader) scan() bool {
|
||||
if r.scanner == nil {
|
||||
r.scanner = bufio.NewScanner(r.Reader)
|
||||
}
|
||||
if len(r.buf) > 0 {
|
||||
return true
|
||||
}
|
||||
if r.scanner.Scan() {
|
||||
r.buf = []byte(Clean(r.scanner.Text(), r.color) + "\n")
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (r *reader) fill(p []byte) int {
|
||||
n := len(r.buf)
|
||||
copy(p, r.buf)
|
||||
if len(p) < len(r.buf) {
|
||||
r.buf = r.buf[len(p):]
|
||||
n = len(p)
|
||||
} else {
|
||||
r.buf = nil
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func (r *reader) Read(p []byte) (int, error) {
|
||||
n := r.fill(p)
|
||||
if n < len(p) {
|
||||
if !r.scan() {
|
||||
if n == 0 {
|
||||
return 0, io.EOF
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
n += r.fill(p[n:])
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
type writer struct {
|
||||
io.Writer
|
||||
buf []byte
|
||||
color bool
|
||||
}
|
||||
|
||||
func NewWriter(w io.Writer, color bool) io.WriteCloser {
|
||||
return &writer{Writer: w, color: color}
|
||||
}
|
||||
|
||||
func (w *writer) Write(p []byte) (int, error) {
|
||||
buf := append(w.buf, p...)
|
||||
lines := bytes.Split(buf, []byte("\n"))
|
||||
if len(lines) > 0 {
|
||||
last := len(lines) - 1
|
||||
w.buf = lines[last]
|
||||
count := 0
|
||||
for _, line := range lines[:last] {
|
||||
n, err := w.Writer.Write([]byte(Clean(string(line), w.color) + "\n"))
|
||||
count += n
|
||||
if err != nil {
|
||||
return count, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (w *writer) Close() error {
|
||||
cl := Clean(string(w.buf), w.color)
|
||||
_, err := w.Writer.Write([]byte(cl))
|
||||
return err
|
||||
}
|
||||
|
|
@ -0,0 +1,113 @@
|
|||
package vtclean
|
||||
|
||||
type char struct {
|
||||
char byte
|
||||
vt100 []byte
|
||||
}
|
||||
|
||||
func chars(p []byte) []char {
|
||||
tmp := make([]char, len(p))
|
||||
for i, v := range p {
|
||||
tmp[i].char = v
|
||||
}
|
||||
return tmp
|
||||
}
|
||||
|
||||
type lineEdit struct {
|
||||
buf []char
|
||||
pos, size int
|
||||
vt100 []byte
|
||||
}
|
||||
|
||||
func newLineEdit(length int) *lineEdit {
|
||||
return &lineEdit{buf: make([]char, length)}
|
||||
}
|
||||
|
||||
func (l *lineEdit) Vt100(p []byte) {
|
||||
l.vt100 = p
|
||||
}
|
||||
|
||||
func (l *lineEdit) Move(x int) {
|
||||
if x < 0 && l.pos <= -x {
|
||||
l.pos = 0
|
||||
} else if x > 0 && l.pos+x > l.size {
|
||||
l.pos = l.size
|
||||
} else {
|
||||
l.pos += x
|
||||
}
|
||||
}
|
||||
|
||||
func (l *lineEdit) MoveAbs(x int) {
|
||||
if x < l.size {
|
||||
l.pos = x
|
||||
}
|
||||
}
|
||||
|
||||
func (l *lineEdit) Write(p []byte) {
|
||||
c := chars(p)
|
||||
if len(c) > 0 {
|
||||
c[0].vt100 = l.vt100
|
||||
l.vt100 = nil
|
||||
}
|
||||
if len(l.buf)-l.pos < len(c) {
|
||||
l.buf = append(l.buf[:l.pos], c...)
|
||||
} else {
|
||||
copy(l.buf[l.pos:], c)
|
||||
}
|
||||
l.pos += len(c)
|
||||
if l.pos > l.size {
|
||||
l.size = l.pos
|
||||
}
|
||||
}
|
||||
|
||||
func (l *lineEdit) Insert(p []byte) {
|
||||
c := chars(p)
|
||||
if len(c) > 0 {
|
||||
c[0].vt100 = l.vt100
|
||||
l.vt100 = nil
|
||||
}
|
||||
l.size += len(c)
|
||||
c = append(c, l.buf[l.pos:]...)
|
||||
l.buf = append(l.buf[:l.pos], c...)
|
||||
}
|
||||
|
||||
func (l *lineEdit) Delete(n int) {
|
||||
most := l.size - l.pos
|
||||
if n > most {
|
||||
n = most
|
||||
}
|
||||
copy(l.buf[l.pos:], l.buf[l.pos+n:])
|
||||
l.size -= n
|
||||
}
|
||||
|
||||
func (l *lineEdit) Clear() {
|
||||
for i := 0; i < len(l.buf); i++ {
|
||||
l.buf[i].char = ' '
|
||||
}
|
||||
}
|
||||
func (l *lineEdit) ClearLeft() {
|
||||
for i := 0; i < l.pos+1; i++ {
|
||||
l.buf[i].char = ' '
|
||||
}
|
||||
}
|
||||
func (l *lineEdit) ClearRight() {
|
||||
l.size = l.pos
|
||||
}
|
||||
|
||||
func (l *lineEdit) Bytes() []byte {
|
||||
length := 0
|
||||
buf := l.buf[:l.size]
|
||||
for _, v := range buf {
|
||||
length += 1 + len(v.vt100)
|
||||
}
|
||||
tmp := make([]byte, 0, length)
|
||||
for _, v := range buf {
|
||||
tmp = append(tmp, v.vt100...)
|
||||
tmp = append(tmp, v.char)
|
||||
}
|
||||
return tmp
|
||||
}
|
||||
|
||||
func (l *lineEdit) String() string {
|
||||
return string(l.Bytes())
|
||||
}
|
||||
|
|
@ -0,0 +1,95 @@
|
|||
package vtclean
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"regexp"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// regex based on ECMA-48:
|
||||
// 1. optional:
|
||||
// one of [ or ]
|
||||
// any amount of 0x30-0x3f
|
||||
// any amount of 0x20-0x2f
|
||||
// 3. exactly one 0x40-0x7e
|
||||
var vt100re = regexp.MustCompile(`^\033([\[\]]([0-9:;<=>\?]*)([!"#$%&'()*+,\-./]*))?([@A-Z\[\]^_\x60a-z{|}~])`)
|
||||
var vt100exc = regexp.MustCompile(`^\033(\[[^a-zA-Z0-9@\?]+|[\(\)]).`)
|
||||
|
||||
// this is to handle the RGB escape generated by `tput initc 1 500 500 500`
|
||||
var vt100long = regexp.MustCompile(`^\033](\d+);([^\033]+)\033\\`)
|
||||
|
||||
func Clean(line string, color bool) string {
|
||||
var edit = newLineEdit(len(line))
|
||||
lineb := []byte(line)
|
||||
|
||||
hadColor := false
|
||||
for i := 0; i < len(lineb); {
|
||||
c := lineb[i]
|
||||
switch c {
|
||||
case '\r':
|
||||
edit.MoveAbs(0)
|
||||
case '\b':
|
||||
edit.Move(-1)
|
||||
case '\033':
|
||||
// set terminal title
|
||||
if bytes.HasPrefix(lineb[i:], []byte("\x1b]0;")) {
|
||||
pos := bytes.Index(lineb[i:], []byte("\a"))
|
||||
if pos != -1 {
|
||||
i += pos + 1
|
||||
continue
|
||||
}
|
||||
}
|
||||
if m := vt100long.Find(lineb[i:]); m != nil {
|
||||
i += len(m)
|
||||
} else if m := vt100exc.Find(lineb[i:]); m != nil {
|
||||
i += len(m)
|
||||
} else if m := vt100re.FindSubmatch(lineb[i:]); m != nil {
|
||||
i += len(m[0])
|
||||
num := string(m[2])
|
||||
n, err := strconv.Atoi(num)
|
||||
if err != nil || n > 10000 {
|
||||
n = 1
|
||||
}
|
||||
switch m[4][0] {
|
||||
case 'm':
|
||||
if color {
|
||||
hadColor = true
|
||||
edit.Vt100(m[0])
|
||||
}
|
||||
case '@':
|
||||
edit.Insert(bytes.Repeat([]byte{' '}, n))
|
||||
case 'G':
|
||||
edit.MoveAbs(n)
|
||||
case 'C':
|
||||
edit.Move(n)
|
||||
case 'D':
|
||||
edit.Move(-n)
|
||||
case 'P':
|
||||
edit.Delete(n)
|
||||
case 'K':
|
||||
switch num {
|
||||
case "", "0":
|
||||
edit.ClearRight()
|
||||
case "1":
|
||||
edit.ClearLeft()
|
||||
case "2":
|
||||
edit.Clear()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
i += 1
|
||||
}
|
||||
continue
|
||||
default:
|
||||
if c == '\n' || c == '\t' || c >= ' ' {
|
||||
edit.Write([]byte{c})
|
||||
}
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
out := edit.Bytes()
|
||||
if hadColor {
|
||||
out = append(out, []byte("\033[0m")...)
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
vendor
|
||||
all-cover.txt
|
||||
bin/
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
run:
|
||||
deadline: 5m
|
||||
|
||||
issues:
|
||||
# Disable maximums so we see all issues
|
||||
max-per-linter: 0
|
||||
max-same-issues: 0
|
||||
|
||||
# golangci-lint ignores missing docstrings by default. That's no good!
|
||||
exclude-use-default: false
|
||||
|
||||
linters:
|
||||
disable-all: true
|
||||
enable:
|
||||
- misspell
|
||||
- golint
|
||||
- goimports
|
||||
- ineffassign
|
||||
- deadcode
|
||||
- gofmt
|
||||
- govet
|
||||
- structcheck
|
||||
- unconvert
|
||||
- megacheck
|
||||
- typecheck
|
||||
- varcheck
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
dist: bionic
|
||||
language: go
|
||||
|
||||
go:
|
||||
- "1.12.x"
|
||||
- "1.13.x"
|
||||
|
||||
branches:
|
||||
only:
|
||||
- master
|
||||
|
||||
after_success:
|
||||
# only report coverage for go-version 1.11
|
||||
- if [[ $TRAVIS_GO_VERSION =~ ^1\.11 ]] ; then bash <(curl -s https://codecov.io/bash) -f all-cover.txt; fi
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
# CHANGELOG
|
||||
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
||||
and this project adheres to [Semantic Versioning](http://semver.org/).
|
||||
|
||||
## Unreleased
|
||||
|
||||
## [0.8.0] - 2020-09-28
|
||||
|
||||
### Added
|
||||
|
||||
- Support ctrl-h for backspace
|
||||
- Allow hiding entered data after submit
|
||||
- Allow masking input with an empty rune to hide input length
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix echo of cursor after input is finished
|
||||
- Better support for keycodes on Windows
|
||||
|
||||
|
||||
## [0.7.0] - 2020-01-11
|
||||
|
||||
### Added
|
||||
|
||||
- Add support for configurable Stdin/Stdout on Prompt
|
||||
- Add support for setting initial cursor position
|
||||
- Switch to golangci-lint for linting
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed support for Go 1.11
|
||||
|
||||
### Fixed
|
||||
|
||||
- Reduce tool-based deps, hopefully fixing any install issues
|
||||
|
||||
## [0.6.0] - 2019-11-29
|
||||
|
||||
### Added
|
||||
|
||||
- Support configurable stdin
|
||||
|
||||
### Fixed
|
||||
|
||||
- Correct the dep on go-i18n
|
||||
|
||||
## [0.5.0] - 2019-11-29
|
||||
|
||||
### Added
|
||||
|
||||
- Now building and testing on go 1.11, go 1.12, and go 1.13
|
||||
|
||||
### Removed
|
||||
|
||||
- Removed support for Go versions that don't include modules.
|
||||
|
||||
## [0.4.0] - 2019-02-19
|
||||
|
||||
### Added
|
||||
|
||||
- The text displayed when an item was successfully selected can be hidden
|
||||
|
||||
## [0.3.2] - 2018-11-26
|
||||
|
||||
### Added
|
||||
|
||||
- Support Go modules
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix typos in PromptTemplates documentation
|
||||
|
||||
## [0.3.1] - 2018-07-26
|
||||
|
||||
### Added
|
||||
|
||||
- Improved documentation for GoDoc
|
||||
- Navigation keys information for Windows
|
||||
|
||||
### Fixed
|
||||
|
||||
- `success` template was not properly displayed after a successful prompt.
|
||||
|
||||
## [0.3.0] - 2018-05-22
|
||||
|
||||
### Added
|
||||
|
||||
- Background colors codes and template helpers
|
||||
- `AllowEdit` for prompt to prevent deletion of the default value by any key
|
||||
- Added `StartInSearchMode` to allow starting the prompt in search mode
|
||||
|
||||
### Fixed
|
||||
|
||||
- `<Enter>` key press on Windows
|
||||
- `juju/ansiterm` dependency
|
||||
- `chzyer/readline#136` new api with ReadCloser
|
||||
- Deleting UTF-8 characters sequence
|
||||
|
||||
## [0.2.1] - 2017-11-30
|
||||
|
||||
### Fixed
|
||||
|
||||
- `SelectWithAdd` panicking on `.Run` due to lack of keys setup
|
||||
- Backspace key on Windows
|
||||
|
||||
## [0.2.0] - 2017-11-16
|
||||
|
||||
### Added
|
||||
|
||||
- `Select` items can now be searched
|
||||
|
||||
## [0.1.0] - 2017-11-02
|
||||
|
||||
### Added
|
||||
|
||||
- extract `promptui` from [torus](https://github.com/manifoldco/torus-cli) as a
|
||||
standalone lib.
|
||||
- `promptui.Prompt` provides a single input line to capture user information.
|
||||
- `promptui.Select` provides a list of options to choose from. Users can
|
||||
navigate through the list either one item at time or by pagination
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
# Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as
|
||||
contributors and maintainers pledge to making participation in our project and
|
||||
our community a harassment-free experience for everyone, regardless of age,
|
||||
body size, disability, ethnicity, gender identity and expression, level of
|
||||
experience, nationality, personal appearance, race, religion, or sexual
|
||||
identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behaviour that contributes to creating a positive environment
|
||||
include:
|
||||
|
||||
- Using welcoming and inclusive language
|
||||
- Being respectful of differing viewpoints and experiences
|
||||
- Gracefully accepting constructive criticism
|
||||
- Focusing on what is best for the community
|
||||
- Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behaviour by participants include:
|
||||
|
||||
- The use of sexualized language or imagery and unwelcome sexual attention or
|
||||
advances
|
||||
- Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
- Public or private harassment
|
||||
- Publishing others' private information, such as a physical or electronic
|
||||
address, without explicit permission
|
||||
- Other conduct which could reasonably be considered inappropriate in a
|
||||
professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable
|
||||
behaviour and are expected to take appropriate and fair corrective action in
|
||||
response to any instances of unacceptable behaviour.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or
|
||||
reject comments, commits, code, wiki edits, issues, and other contributions
|
||||
that are not aligned to this Code of Conduct, or to ban temporarily or
|
||||
permanently any contributor for other behaviours that they deem inappropriate,
|
||||
threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces
|
||||
when an individual is representing the project or its community. Examples of
|
||||
representing a project or community include using an official project e-mail
|
||||
address, posting via an official social media account, or acting as an
|
||||
appointed representative at an online or offline event. Representation of a
|
||||
project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at
|
||||
[hello@manifold.co](mailto:hello@manifold.co). All complaints will be reviewed
|
||||
and investigated and will result in a response that is deemed necessary and
|
||||
appropriate to the circumstances. The project team is obligated to maintain
|
||||
confidentiality with regard to the reporter of an incident. Further details of
|
||||
specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good
|
||||
faith may face temporary or permanent repercussions as determined by other
|
||||
members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the Contributor Covenant, version 1.4,
|
||||
available at
|
||||
[http://contributor-covenant.org/version/1/4](http://contributor-covenant.org/version/1/4).
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
BSD 3-Clause License
|
||||
|
||||
Copyright (c) 2017, Arigato Machine Inc.
|
||||
All rights reserved.
|
||||
|
||||
Redistribution and use in source and binary forms, with or without
|
||||
modification, are permitted provided that the following conditions are met:
|
||||
|
||||
* Redistributions of source code must retain the above copyright notice, this
|
||||
list of conditions and the following disclaimer.
|
||||
|
||||
* Redistributions in binary form must reproduce the above copyright notice,
|
||||
this list of conditions and the following disclaimer in the documentation
|
||||
and/or other materials provided with the distribution.
|
||||
|
||||
* Neither the name of the copyright holder nor the names of its
|
||||
contributors may be used to endorse or promote products derived from
|
||||
this software without specific prior written permission.
|
||||
|
||||
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
||||
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
||||
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
||||
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
||||
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
||||
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
||||
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
||||
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
||||
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
export GO111MODULE := on
|
||||
export PATH := ./bin:$(PATH)
|
||||
|
||||
ci: bootstrap lint cover
|
||||
.PHONY: ci
|
||||
|
||||
#################################################
|
||||
# Bootstrapping for base golang package and tool deps
|
||||
#################################################
|
||||
|
||||
bootstrap:
|
||||
curl -sfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh| sh -s v1.21.0
|
||||
.PHONY: bootstrap
|
||||
|
||||
mod-update:
|
||||
go get -u -m
|
||||
go mod tidy
|
||||
|
||||
mod-tidy:
|
||||
go mod tidy
|
||||
|
||||
.PHONY: $(CMD_PKGS)
|
||||
.PHONY: mod-update mod-tidy
|
||||
|
||||
#################################################
|
||||
# Test and linting
|
||||
#################################################
|
||||
# Run all the linters
|
||||
lint:
|
||||
bin/golangci-lint run ./...
|
||||
.PHONY: lint
|
||||
|
||||
test:
|
||||
CGO_ENABLED=0 go test $$(go list ./... | grep -v generated)
|
||||
.PHONY: test
|
||||
|
||||
COVER_TEST_PKGS:=$(shell find . -type f -name '*_test.go' | rev | cut -d "/" -f 2- | rev | grep -v generated | sort -u)
|
||||
$(COVER_TEST_PKGS:=-cover): %-cover: all-cover.txt
|
||||
@CGO_ENABLED=0 go test -v -coverprofile=$@.out -covermode=atomic ./$*
|
||||
@if [ -f $@.out ]; then \
|
||||
grep -v "mode: atomic" < $@.out >> all-cover.txt; \
|
||||
rm $@.out; \
|
||||
fi
|
||||
|
||||
all-cover.txt:
|
||||
echo "mode: atomic" > all-cover.txt
|
||||
|
||||
cover: all-cover.txt $(COVER_TEST_PKGS:=-cover)
|
||||
.PHONY: cover all-cover.txt
|
||||
|
|
@ -0,0 +1,107 @@
|
|||
# promptui
|
||||
|
||||
Interactive prompt for command-line applications.
|
||||
|
||||
We built Promptui because we wanted to make it easy and fun to explore cloud
|
||||
services with [manifold cli](https://github.com/manifoldco/manifold-cli).
|
||||
|
||||
[Code of Conduct](./CODE_OF_CONDUCT.md) |
|
||||
[Contribution Guidelines](./.github/CONTRIBUTING.md)
|
||||
|
||||
[](https://github.com/manifoldco/promptui/releases)
|
||||
[](https://godoc.org/github.com/manifoldco/promptui)
|
||||
[](https://travis-ci.org/manifoldco/promptui)
|
||||
[](https://goreportcard.com/report/github.com/manifoldco/promptui)
|
||||
[](./LICENSE.md)
|
||||
|
||||
## Overview
|
||||
|
||||

|
||||
|
||||
Promptui is a library providing a simple interface to create command-line
|
||||
prompts for go. It can be easily integrated into
|
||||
[spf13/cobra](https://github.com/spf13/cobra),
|
||||
[urfave/cli](https://github.com/urfave/cli) or any cli go application.
|
||||
|
||||
Promptui has two main input modes:
|
||||
|
||||
- `Prompt` provides a single line for user input. Prompt supports
|
||||
optional live validation, confirmation and masking the input.
|
||||
|
||||
- `Select` provides a list of options to choose from. Select supports
|
||||
pagination, search, detailed view and custom templates.
|
||||
|
||||
For a full list of options check [GoDoc](https://godoc.org/github.com/manifoldco/promptui).
|
||||
|
||||
## Basic Usage
|
||||
|
||||
### Prompt
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
"github.com/manifoldco/promptui"
|
||||
)
|
||||
|
||||
func main() {
|
||||
validate := func(input string) error {
|
||||
_, err := strconv.ParseFloat(input, 64)
|
||||
if err != nil {
|
||||
return errors.New("Invalid number")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
prompt := promptui.Prompt{
|
||||
Label: "Number",
|
||||
Validate: validate,
|
||||
}
|
||||
|
||||
result, err := prompt.Run()
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Prompt failed %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("You choose %q\n", result)
|
||||
}
|
||||
```
|
||||
|
||||
### Select
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/manifoldco/promptui"
|
||||
)
|
||||
|
||||
func main() {
|
||||
prompt := promptui.Select{
|
||||
Label: "Select Day",
|
||||
Items: []string{"Monday", "Tuesday", "Wednesday", "Thursday", "Friday",
|
||||
"Saturday", "Sunday"},
|
||||
}
|
||||
|
||||
_, result, err := prompt.Run()
|
||||
|
||||
if err != nil {
|
||||
fmt.Printf("Prompt failed %v\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
fmt.Printf("You choose %q\n", result)
|
||||
}
|
||||
```
|
||||
|
||||
### More Examples
|
||||
|
||||
See full list of [examples](https://github.com/manifoldco/promptui/tree/master/_examples)
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
package promptui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"text/template"
|
||||
)
|
||||
|
||||
const esc = "\033["
|
||||
|
||||
type attribute int
|
||||
|
||||
// The possible state of text inside the application, either Bold, faint, italic or underline.
|
||||
//
|
||||
// These constants are called through the use of the Styler function.
|
||||
const (
|
||||
reset attribute = iota
|
||||
|
||||
FGBold
|
||||
FGFaint
|
||||
FGItalic
|
||||
FGUnderline
|
||||
)
|
||||
|
||||
// The possible colors of text inside the application.
|
||||
//
|
||||
// These constants are called through the use of the Styler function.
|
||||
const (
|
||||
FGBlack attribute = iota + 30
|
||||
FGRed
|
||||
FGGreen
|
||||
FGYellow
|
||||
FGBlue
|
||||
FGMagenta
|
||||
FGCyan
|
||||
FGWhite
|
||||
)
|
||||
|
||||
// The possible background colors of text inside the application.
|
||||
//
|
||||
// These constants are called through the use of the Styler function.
|
||||
const (
|
||||
BGBlack attribute = iota + 40
|
||||
BGRed
|
||||
BGGreen
|
||||
BGYellow
|
||||
BGBlue
|
||||
BGMagenta
|
||||
BGCyan
|
||||
BGWhite
|
||||
)
|
||||
|
||||
// ResetCode is the character code used to reset the terminal formatting
|
||||
var ResetCode = fmt.Sprintf("%s%dm", esc, reset)
|
||||
|
||||
const (
|
||||
hideCursor = esc + "?25l"
|
||||
showCursor = esc + "?25h"
|
||||
clearLine = esc + "2K"
|
||||
)
|
||||
|
||||
// FuncMap defines template helpers for the output. It can be extended as a regular map.
|
||||
//
|
||||
// The functions inside the map link the state, color and background colors strings detected in templates to a Styler
|
||||
// function that applies the given style using the corresponding constant.
|
||||
var FuncMap = template.FuncMap{
|
||||
"black": Styler(FGBlack),
|
||||
"red": Styler(FGRed),
|
||||
"green": Styler(FGGreen),
|
||||
"yellow": Styler(FGYellow),
|
||||
"blue": Styler(FGBlue),
|
||||
"magenta": Styler(FGMagenta),
|
||||
"cyan": Styler(FGCyan),
|
||||
"white": Styler(FGWhite),
|
||||
"bgBlack": Styler(BGBlack),
|
||||
"bgRed": Styler(BGRed),
|
||||
"bgGreen": Styler(BGGreen),
|
||||
"bgYellow": Styler(BGYellow),
|
||||
"bgBlue": Styler(BGBlue),
|
||||
"bgMagenta": Styler(BGMagenta),
|
||||
"bgCyan": Styler(BGCyan),
|
||||
"bgWhite": Styler(BGWhite),
|
||||
"bold": Styler(FGBold),
|
||||
"faint": Styler(FGFaint),
|
||||
"italic": Styler(FGItalic),
|
||||
"underline": Styler(FGUnderline),
|
||||
}
|
||||
|
||||
func upLine(n uint) string {
|
||||
return movementCode(n, 'A')
|
||||
}
|
||||
|
||||
func movementCode(n uint, code rune) string {
|
||||
return esc + strconv.FormatUint(uint64(n), 10) + string(code)
|
||||
}
|
||||
|
||||
// Styler is a function that accepts multiple possible styling transforms from the state,
|
||||
// color and background colors constants and transforms them into a templated string
|
||||
// to apply those styles in the CLI.
|
||||
//
|
||||
// The returned styling function accepts a string that will be extended with
|
||||
// the wrapping function's styling attributes.
|
||||
func Styler(attrs ...attribute) func(interface{}) string {
|
||||
attrstrs := make([]string, len(attrs))
|
||||
for i, v := range attrs {
|
||||
attrstrs[i] = strconv.Itoa(int(v))
|
||||
}
|
||||
|
||||
seq := strings.Join(attrstrs, ";")
|
||||
|
||||
return func(v interface{}) string {
|
||||
end := ""
|
||||
s, ok := v.(string)
|
||||
if !ok || !strings.HasSuffix(s, ResetCode) {
|
||||
end = ResetCode
|
||||
}
|
||||
return fmt.Sprintf("%s%sm%v%s", esc, seq, v, end)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,232 @@
|
|||
package promptui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Pointer is A specific type that translates a given set of runes into a given
|
||||
// set of runes pointed at by the cursor.
|
||||
type Pointer func(to []rune) []rune
|
||||
|
||||
func defaultCursor(ignored []rune) []rune {
|
||||
return []rune("\u2588")
|
||||
}
|
||||
|
||||
func blockCursor(input []rune) []rune {
|
||||
return []rune(fmt.Sprintf("\\e[7m%s\\e[0m", string(input)))
|
||||
}
|
||||
|
||||
func pipeCursor(input []rune) []rune {
|
||||
marker := []rune("|")
|
||||
out := []rune{}
|
||||
out = append(out, marker...)
|
||||
out = append(out, input...)
|
||||
return out
|
||||
}
|
||||
|
||||
var (
|
||||
// DefaultCursor is a big square block character. Obscures whatever was
|
||||
// input.
|
||||
DefaultCursor Pointer = defaultCursor
|
||||
// BlockCursor is a cursor which highlights a character by inverting colors
|
||||
// on it.
|
||||
BlockCursor Pointer = blockCursor
|
||||
// PipeCursor is a pipe character "|" which appears before the input
|
||||
// character.
|
||||
PipeCursor Pointer = pipeCursor
|
||||
)
|
||||
|
||||
// Cursor tracks the state associated with the movable cursor
|
||||
// The strategy is to keep the prompt, input pristine except for requested
|
||||
// modifications. The insertion of the cursor happens during a `format` call
|
||||
// and we read in new input via an `Update` call
|
||||
type Cursor struct {
|
||||
// shows where the user inserts/updates text
|
||||
Cursor Pointer
|
||||
// what the user entered, and what we will echo back to them, after
|
||||
// insertion of the cursor and prefixing with the prompt
|
||||
input []rune
|
||||
// Put the cursor before this slice
|
||||
Position int
|
||||
erase bool
|
||||
}
|
||||
|
||||
// NewCursor create a new cursor, with the DefaultCursor, the specified input,
|
||||
// and position at the end of the specified starting input.
|
||||
func NewCursor(startinginput string, pointer Pointer, eraseDefault bool) Cursor {
|
||||
if pointer == nil {
|
||||
pointer = defaultCursor
|
||||
}
|
||||
cur := Cursor{Cursor: pointer, Position: len(startinginput), input: []rune(startinginput), erase: eraseDefault}
|
||||
if eraseDefault {
|
||||
cur.Start()
|
||||
} else {
|
||||
cur.End()
|
||||
}
|
||||
return cur
|
||||
}
|
||||
|
||||
func (c *Cursor) String() string {
|
||||
return fmt.Sprintf(
|
||||
"Cursor: %s, input %s, Position %d",
|
||||
string(c.Cursor([]rune(""))), string(c.input), c.Position)
|
||||
}
|
||||
|
||||
// End is a convenience for c.Place(len(c.input)) so you don't have to know how I
|
||||
// indexed.
|
||||
func (c *Cursor) End() {
|
||||
c.Place(len(c.input))
|
||||
}
|
||||
|
||||
// Start is convenience for c.Place(0) so you don't have to know how I
|
||||
// indexed.
|
||||
func (c *Cursor) Start() {
|
||||
c.Place(0)
|
||||
}
|
||||
|
||||
// ensures we are in bounds.
|
||||
func (c *Cursor) correctPosition() {
|
||||
if c.Position > len(c.input) {
|
||||
c.Position = len(c.input)
|
||||
}
|
||||
|
||||
if c.Position < 0 {
|
||||
c.Position = 0
|
||||
}
|
||||
}
|
||||
|
||||
// insert the cursor rune array into r before the provided index
|
||||
func format(a []rune, c *Cursor) string {
|
||||
i := c.Position
|
||||
var b []rune
|
||||
|
||||
out := make([]rune, 0)
|
||||
if i < len(a) {
|
||||
b = c.Cursor(a[i : i+1])
|
||||
out = append(out, a[:i]...) // does not include i
|
||||
out = append(out, b...) // add the cursor
|
||||
out = append(out, a[i+1:]...) // add the rest after i
|
||||
} else {
|
||||
b = c.Cursor([]rune{})
|
||||
out = append(out, a...)
|
||||
out = append(out, b...)
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
// Format renders the input with the Cursor appropriately positioned.
|
||||
func (c *Cursor) Format() string {
|
||||
r := c.input
|
||||
// insert the cursor
|
||||
return format(r, c)
|
||||
}
|
||||
|
||||
// FormatMask replaces all input runes with the mask rune.
|
||||
func (c *Cursor) FormatMask(mask rune) string {
|
||||
if mask == ' ' {
|
||||
return format([]rune{}, c)
|
||||
}
|
||||
|
||||
r := make([]rune, len(c.input))
|
||||
for i := range r {
|
||||
r[i] = mask
|
||||
}
|
||||
return format(r, c)
|
||||
}
|
||||
|
||||
// Update inserts newinput into the input []rune in the appropriate place.
|
||||
// The cursor is moved to the end of the inputed sequence.
|
||||
func (c *Cursor) Update(newinput string) {
|
||||
a := c.input
|
||||
b := []rune(newinput)
|
||||
i := c.Position
|
||||
a = append(a[:i], append(b, a[i:]...)...)
|
||||
c.input = a
|
||||
c.Move(len(b))
|
||||
}
|
||||
|
||||
// Get returns a copy of the input
|
||||
func (c *Cursor) Get() string {
|
||||
return string(c.input)
|
||||
}
|
||||
|
||||
// GetMask returns a mask string with length equal to the input
|
||||
func (c *Cursor) GetMask(mask rune) string {
|
||||
return strings.Repeat(string(mask), len(c.input))
|
||||
}
|
||||
|
||||
// Replace replaces the previous input with whatever is specified, and moves the
|
||||
// cursor to the end position
|
||||
func (c *Cursor) Replace(input string) {
|
||||
c.input = []rune(input)
|
||||
c.End()
|
||||
}
|
||||
|
||||
// Place moves the cursor to the absolute array index specified by position
|
||||
func (c *Cursor) Place(position int) {
|
||||
c.Position = position
|
||||
c.correctPosition()
|
||||
}
|
||||
|
||||
// Move moves the cursor over in relative terms, by shift indices.
|
||||
func (c *Cursor) Move(shift int) {
|
||||
// delete the current cursor
|
||||
c.Position = c.Position + shift
|
||||
c.correctPosition()
|
||||
}
|
||||
|
||||
// Backspace removes the rune that precedes the cursor
|
||||
//
|
||||
// It handles being at the beginning or end of the row, and moves the cursor to
|
||||
// the appropriate position.
|
||||
func (c *Cursor) Backspace() {
|
||||
a := c.input
|
||||
i := c.Position
|
||||
if i == 0 {
|
||||
// Shrug
|
||||
return
|
||||
}
|
||||
if i == len(a) {
|
||||
c.input = a[:i-1]
|
||||
} else {
|
||||
c.input = append(a[:i-1], a[i:]...)
|
||||
}
|
||||
// now it's pointing to the i+1th element
|
||||
c.Move(-1)
|
||||
}
|
||||
|
||||
// Listen is a readline Listener that updates internal cursor state appropriately.
|
||||
func (c *Cursor) Listen(line []rune, pos int, key rune) ([]rune, int, bool) {
|
||||
if line != nil {
|
||||
// no matter what, update our internal representation.
|
||||
c.Update(string(line))
|
||||
}
|
||||
|
||||
switch key {
|
||||
case 0: // empty
|
||||
case KeyEnter:
|
||||
return []rune(c.Get()), c.Position, false
|
||||
case KeyBackspace, KeyCtrlH:
|
||||
if c.erase {
|
||||
c.erase = false
|
||||
c.Replace("")
|
||||
}
|
||||
c.Backspace()
|
||||
case KeyForward:
|
||||
// the user wants to edit the default, despite how we set it up. Let
|
||||
// them.
|
||||
c.erase = false
|
||||
c.Move(1)
|
||||
case KeyBackward:
|
||||
c.Move(-1)
|
||||
default:
|
||||
if c.erase {
|
||||
c.erase = false
|
||||
c.Replace("")
|
||||
c.Update(string(key))
|
||||
}
|
||||
}
|
||||
|
||||
return []rune(c.Get()), c.Position, true
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
module github.com/manifoldco/promptui
|
||||
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/chzyer/logex v1.1.10 // indirect
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 // indirect
|
||||
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a
|
||||
github.com/kr/pretty v0.1.0 // indirect
|
||||
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a // indirect
|
||||
github.com/mattn/go-colorable v0.0.9 // indirect
|
||||
github.com/mattn/go-isatty v0.0.4 // indirect
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
|
||||
)
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
github.com/chzyer/logex v1.1.10 h1:Swpa1K6QvQznwJRcfTfQJmTE72DqScAa40E+fbHEXEE=
|
||||
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e h1:fY5BOSpyZCqRo5OhCuC+XN+r/bBCmeuuJtjz+bCNIf8=
|
||||
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1 h1:q763qf9huN11kDQavWsoZXJNW3xEE4JJyHa5Q25/sd8=
|
||||
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
|
||||
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a h1:FaWFmfWdAUKbSCtOU2QjDaorUexogfaMgbipgYATUMU=
|
||||
github.com/juju/ansiterm v0.0.0-20180109212912-720a0952cc2a/go.mod h1:UJSiEoRfvx3hP73CvoARgeLjaIOjybY9vj8PUPPFGeU=
|
||||
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a h1:weJVJJRzAJBFRlAiJQROKQs8oC9vOxvm4rZmBBk0ONw=
|
||||
github.com/lunixbochs/vtclean v0.0.0-20180621232353-2d01aacdc34a/go.mod h1:pHhQNgMf3btfWnGBVipUOjRYhoOsdGqdm/+2c2E2WMI=
|
||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs=
|
||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b h1:MQE+LT/ABUuuvEZ+YQAMSXindAdUh7slEmAkup74op4=
|
||||
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package promptui
|
||||
|
||||
import "github.com/chzyer/readline"
|
||||
|
||||
// These runes are used to identify the commands entered by the user in the command prompt. They map
|
||||
// to specific actions of promptui in prompt mode and can be remapped if necessary.
|
||||
var (
|
||||
// KeyEnter is the default key for submission/selection.
|
||||
KeyEnter rune = readline.CharEnter
|
||||
|
||||
// KeyCtrlH is the key for deleting input text.
|
||||
KeyCtrlH rune = readline.CharCtrlH
|
||||
|
||||
// KeyPrev is the default key to go up during selection.
|
||||
KeyPrev rune = readline.CharPrev
|
||||
KeyPrevDisplay = "↑"
|
||||
|
||||
// KeyNext is the default key to go down during selection.
|
||||
KeyNext rune = readline.CharNext
|
||||
KeyNextDisplay = "↓"
|
||||
|
||||
// KeyBackward is the default key to page up during selection.
|
||||
KeyBackward rune = readline.CharBackward
|
||||
KeyBackwardDisplay = "←"
|
||||
|
||||
// KeyForward is the default key to page down during selection.
|
||||
KeyForward rune = readline.CharForward
|
||||
KeyForwardDisplay = "→"
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue