v0.5.0: initial publishable version
This commit is contained in:
parent
9254e307e3
commit
8a699044be
|
@ -1,3 +1,6 @@
|
|||
/pathman
|
||||
dist
|
||||
|
||||
# ---> Go
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
|
|
83
README.md
83
README.md
|
@ -1,3 +1,82 @@
|
|||
# go-envpath
|
||||
# [pathman](https://git.rootprojects.org/root/pathman)
|
||||
|
||||
Manage PATH on Windows, Mac, and Linux with various Shells
|
||||
Manage PATH on Windows, Mac, and Linux with various Shells
|
||||
|
||||
```bash
|
||||
pathman list
|
||||
pathman add ~/.local/bin
|
||||
pathman remove ~/.local/bin
|
||||
```
|
||||
|
||||
Windows: stores PATH in the registry.
|
||||
|
||||
Mac & Linux: stores PATH in `~/.config/envman/PATH.sh`
|
||||
|
||||
# add
|
||||
|
||||
```bash
|
||||
pathman add ~/.local/bin
|
||||
```
|
||||
|
||||
```txt
|
||||
Saved PATH changes. To set the PATH immediately, update the current session:
|
||||
|
||||
export PATH="/Users/me/.local/bin:$PATH"
|
||||
```
|
||||
|
||||
# remove
|
||||
|
||||
```bash
|
||||
pathman remove ~/.local/bin
|
||||
```
|
||||
|
||||
```txt
|
||||
Saved PATH changes. To set the PATH immediately, update the current session:
|
||||
|
||||
export PATH="/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin"
|
||||
```
|
||||
|
||||
# list
|
||||
|
||||
```bash
|
||||
pathman list
|
||||
```
|
||||
|
||||
```txt
|
||||
pathman-managed PATH entries:
|
||||
|
||||
$HOME/.local/bin
|
||||
|
||||
other PATH entries:
|
||||
|
||||
/usr/local/bin
|
||||
/usr/bin
|
||||
/bin
|
||||
/usr/sbin
|
||||
/sbin
|
||||
|
||||
```
|
||||
|
||||
# Windows
|
||||
|
||||
You can use `~` as a shortcut for `%USERPROFILE%`.
|
||||
|
||||
```bash
|
||||
pathman add ~\.local\bin
|
||||
```
|
||||
|
||||
The registry will be used, even when your using Node Bash, Git Bash, or MINGW.
|
||||
|
||||
# build
|
||||
|
||||
```bash
|
||||
git clone https://git.rootprojects.org/root/pathman.git
|
||||
```
|
||||
|
||||
```bash
|
||||
go mod tidy
|
||||
go mod vendor
|
||||
go generate -mod=vendor ./...
|
||||
go build -mod=vendor
|
||||
./pathman list
|
||||
```
|
||||
|
|
|
@ -0,0 +1,43 @@
|
|||
#GOOS=windows GOARCH=amd64 go install
|
||||
#go tool dist list
|
||||
|
||||
# TODO move this into tools/build.go
|
||||
|
||||
export CGO_ENABLED=0
|
||||
exe=pathman
|
||||
gocmd=.
|
||||
|
||||
echo ""
|
||||
go generate -mod=vendor ./...
|
||||
|
||||
echo ""
|
||||
echo "Windows amd64"
|
||||
GOOS=windows GOARCH=amd64 go build -mod=vendor -o dist/windows/amd64/${exe}.exe -ldflags "-s -w -H=windowsgui" $gocmd
|
||||
GOOS=windows GOARCH=amd64 go build -mod=vendor -o dist/windows/amd64/${exe}.debug.exe
|
||||
echo "Windows 386"
|
||||
GOOS=windows GOARCH=386 go build -mod=vendor -o dist/windows/386/${exe}.exe -ldflags "-s -w -H=windowsgui" $gocmd
|
||||
GOOS=windows GOARCH=386 go build -mod=vendor -o dist/windows/386/${exe}.debug.exe
|
||||
|
||||
echo ""
|
||||
echo "Darwin (macOS) amd64"
|
||||
GOOS=darwin GOARCH=amd64 go build -mod=vendor -o dist/darwin/amd64/${exe} -ldflags "-s -w" $gocmd
|
||||
|
||||
echo ""
|
||||
echo "Linux amd64"
|
||||
GOOS=linux GOARCH=amd64 go build -mod=vendor -o dist/linux/amd64/${exe} -ldflags "-s -w" $gocmd
|
||||
echo "Linux 386"
|
||||
GOOS=linux GOARCH=386 go build -mod=vendor -o dist/linux/386/${exe} -ldflags "-s -w" $gocmd
|
||||
|
||||
echo ""
|
||||
echo "RPi 4 (64-bit) ARMv8"
|
||||
GOOS=linux GOARCH=arm64 go build -mod=vendor -o dist/linux/armv8/${exe} -ldflags "-s -w" $gocmd
|
||||
echo "RPi 3 B+ ARMv7"
|
||||
GOOS=linux GOARCH=arm GOARM=7 go build -mod=vendor -o dist/linux/armv7/${exe} -ldflags "-s -w" $gocmd
|
||||
echo "ARMv6"
|
||||
GOOS=linux GOARCH=arm GOARM=6 go build -mod=vendor -o dist/linux/armv6/${exe} -ldflags "-s -w" $gocmd
|
||||
echo "RPi Zero ARMv5"
|
||||
GOOS=linux GOARCH=arm GOARM=5 go build -mod=vendor -o dist/linux/armv5/${exe} -ldflags "-s -w" $gocmd
|
||||
|
||||
echo ""
|
||||
#rsync -av ./dist/ ubuntu@rootprojects.org:/srv/www/rootprojects.org/pathman/dist/
|
||||
# https://rootprojects.org/pathman/dist/windows/amd64/pathman.exe
|
|
@ -48,12 +48,12 @@ func Add(entry string) (bool, error) {
|
|||
return false, err
|
||||
}
|
||||
|
||||
_, ok := isInPath(home, paths, pathentry)
|
||||
if ok {
|
||||
index := IndexOf(paths, pathentry)
|
||||
if index >= 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
paths = append([]string{pathentry}, paths...)
|
||||
paths = append(paths, pathentry)
|
||||
err = writeEnv(fullpath, paths)
|
||||
if nil != err {
|
||||
return false, err
|
||||
|
@ -86,8 +86,8 @@ func Remove(entry string) (bool, error) {
|
|||
return false, err
|
||||
}
|
||||
|
||||
index, exists := isInPath(home, oldpaths, pathentry)
|
||||
if !exists {
|
||||
index := IndexOf(oldpaths, pathentry)
|
||||
if index < 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
|
@ -119,7 +119,8 @@ func getEnv(home string, env string) (string, []string, error) {
|
|||
return "", nil, err
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("00-%s.env", env)
|
||||
//filename := fmt.Sprintf("00-%s.env", env)
|
||||
filename := fmt.Sprintf("%s.env", env)
|
||||
for i := range nodes {
|
||||
name := nodes[i].Name()
|
||||
if fmt.Sprintf("%s.env", env) == name || strings.HasSuffix(name, fmt.Sprintf("-%s.env", env)) {
|
||||
|
@ -188,19 +189,24 @@ func writeEnv(fullpath string, paths []string) error {
|
|||
return f.Close()
|
||||
}
|
||||
|
||||
func isInPath(home string, paths []string, pathentry string) (int, bool) {
|
||||
// IndexOf searches the given path list for first occurence
|
||||
// of the given path entry and returns the index, or -1
|
||||
func IndexOf(paths []string, p string) int {
|
||||
home, err := os.UserHomeDir()
|
||||
if nil != err {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
p, _ = normalizePathEntry(home, p)
|
||||
index := -1
|
||||
for i := range paths {
|
||||
entry, _ := normalizePathEntry(home, paths[i])
|
||||
if pathentry == entry {
|
||||
if p == entry {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index >= 0 {
|
||||
return index, true
|
||||
}
|
||||
return -1, false
|
||||
return index
|
||||
}
|
||||
|
||||
func normalizePathEntry(home, pathentry string) (string, error) {
|
||||
|
|
|
@ -2,6 +2,8 @@ package envpath
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
|
@ -11,9 +13,6 @@ func TestAddRemove(t *testing.T) {
|
|||
t.Error(err)
|
||||
return
|
||||
}
|
||||
for i := range paths {
|
||||
fmt.Println(paths[i])
|
||||
}
|
||||
|
||||
modified, err := Remove("/tmp/doesnt/exist")
|
||||
if nil != err {
|
||||
|
@ -35,10 +34,16 @@ func TestAddRemove(t *testing.T) {
|
|||
return
|
||||
}
|
||||
|
||||
var exists bool
|
||||
paths, err = Paths()
|
||||
if 1 != len(paths) || "/tmp/delete/me" != paths[0] {
|
||||
for i := range paths {
|
||||
if "/tmp/delete/me" == paths[i] {
|
||||
exists = true
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
fmt.Println("len(paths):", len(paths))
|
||||
t.Error(fmt.Errorf("Paths: should have had exactly one entry: /tmp/delete/me"))
|
||||
t.Error(fmt.Errorf("Paths: should have had the entry: /tmp/delete/me"))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -52,9 +57,16 @@ func TestAddRemove(t *testing.T) {
|
|||
return
|
||||
}
|
||||
|
||||
exists = false
|
||||
paths, err = Paths()
|
||||
if 1 != len(paths) || "/tmp/delete/me" != paths[0] {
|
||||
t.Error(fmt.Errorf("Paths: should have had exactly one entry: /tmp/delete/me"))
|
||||
for i := range paths {
|
||||
if "/tmp/delete/me" == paths[i] {
|
||||
exists = true
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
fmt.Println("len(paths):", len(paths))
|
||||
t.Error(fmt.Errorf("Paths: should have had the entry: /tmp/delete/me"))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -78,9 +90,16 @@ func TestAddRemove(t *testing.T) {
|
|||
return
|
||||
}
|
||||
|
||||
exists = false
|
||||
paths, err = Paths()
|
||||
if 0 != len(paths) {
|
||||
t.Error(fmt.Errorf("Paths: should have had no entries"))
|
||||
for i := range paths {
|
||||
if "/tmp/delete/me" == paths[i] {
|
||||
exists = true
|
||||
}
|
||||
}
|
||||
if exists {
|
||||
fmt.Println("len(paths):", len(paths))
|
||||
t.Error(fmt.Errorf("Paths: should not have had the entry: /tmp/delete/me"))
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -94,3 +113,73 @@ func TestAddRemove(t *testing.T) {
|
|||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestHome(t *testing.T) {
|
||||
home, _ := os.UserHomeDir()
|
||||
|
||||
modified, err := Add(filepath.Join(home, "deleteme"))
|
||||
if nil != err {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if !modified {
|
||||
t.Error(fmt.Errorf("Add $HOME/deleteme: should have modified"))
|
||||
return
|
||||
}
|
||||
|
||||
modified, err = Add(filepath.Join(home, "deleteme"))
|
||||
if nil != err {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if modified {
|
||||
t.Error(fmt.Errorf("Add $HOME/deleteme: should not have modified"))
|
||||
return
|
||||
}
|
||||
|
||||
exists := false
|
||||
paths, err := Paths()
|
||||
for i := range paths {
|
||||
if "$HOME/deleteme" == paths[i] {
|
||||
exists = true
|
||||
}
|
||||
}
|
||||
if !exists {
|
||||
fmt.Println("len(paths):", len(paths))
|
||||
t.Error(fmt.Errorf("Paths: should have had the entry: $HOME/deleteme"))
|
||||
return
|
||||
}
|
||||
|
||||
modified, err = Remove(filepath.Join(home, "deleteme"))
|
||||
if nil != err {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if !modified {
|
||||
t.Error(fmt.Errorf("Remove $HOME/deleteme: should have modified"))
|
||||
return
|
||||
}
|
||||
|
||||
exists = false
|
||||
paths, err = Paths()
|
||||
for i := range paths {
|
||||
if "$HOME/deleteme" == paths[i] {
|
||||
exists = true
|
||||
}
|
||||
}
|
||||
if exists {
|
||||
fmt.Println("len(paths):", len(paths))
|
||||
t.Error(fmt.Errorf("Paths: should not have had the entry: $HOME/deleteme"))
|
||||
return
|
||||
}
|
||||
|
||||
modified, err = Remove(filepath.Join(home, "deleteme"))
|
||||
if nil != err {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if modified {
|
||||
t.Error(fmt.Errorf("Remove $HOME/deleteme: should not have modified"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
|
|
@ -72,7 +72,7 @@ func initializeShells(home string) error {
|
|||
for i := range confs {
|
||||
c := confs[i]
|
||||
|
||||
if os.Getenv("SHELL") == c.shell {
|
||||
if filepath.Base(os.Getenv("SHELL")) == c.shell {
|
||||
nativeMatch = c
|
||||
}
|
||||
|
||||
|
@ -181,7 +181,7 @@ func (c *envConfig) initializeShell() (bool, error) {
|
|||
}
|
||||
|
||||
// Generate our script
|
||||
script := fmt.Sprintf("# Generated for envpath. Do not edit.\n%s\n", c.rcScript)
|
||||
script := fmt.Sprintf("# Generated for envman. Do not edit.\n%s\n", c.rcScript)
|
||||
|
||||
// If there's not a newline before our template,
|
||||
// include it in the template. We want nice things.
|
||||
|
@ -212,7 +212,7 @@ func (c *envConfig) ensurePathsLoader() error {
|
|||
// TODO maybe don't write every time
|
||||
if err := ioutil.WriteFile(
|
||||
loadFile,
|
||||
[]byte(fmt.Sprintf("# Generated for envpath. Do not edit.\n%s\n", c.loadScript)),
|
||||
[]byte(fmt.Sprintf("# Generated for envman. Do not edit.\n%s\n", c.loadScript)),
|
||||
os.FileMode(0755),
|
||||
); nil != err {
|
||||
return err
|
||||
|
|
|
@ -35,17 +35,16 @@ PATH=""
|
|||
|
||||
`
|
||||
|
||||
var paths = []string{
|
||||
`PATH="/foo"`,
|
||||
`PATH="/foo:$PATH"`,
|
||||
`PATH=""`,
|
||||
`PATH="/boo:$PATH"`,
|
||||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
exppaths := []string{
|
||||
`PATH="/foo"`,
|
||||
`PATH="/foo:$PATH"`,
|
||||
`PATH=""`,
|
||||
`PATH="/boo:$PATH"`,
|
||||
}
|
||||
newlines, warnings := Parse([]byte(file), "PATH")
|
||||
newfile := `PATH="` + strings.Join(newlines, "\"\n\tPATH=\"") + `"`
|
||||
expfile := strings.Join(paths, "\n\t")
|
||||
expfile := strings.Join(exppaths, "\n\t")
|
||||
if newfile != expfile {
|
||||
t.Errorf("\nExpected:\n\t%s\nGot:\n\t%s", expfile, newfile)
|
||||
}
|
||||
|
|
5
go.mod
5
go.mod
|
@ -2,4 +2,7 @@ module git.rootprojects.org/root/pathman
|
|||
|
||||
go 1.12
|
||||
|
||||
require golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7
|
||||
require (
|
||||
git.rootprojects.org/root/go-gitver v1.1.3
|
||||
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7
|
||||
)
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
git.rootprojects.org/root/go-gitver v1.1.3 h1:/qR9z53vY+IFhWRxLkF9cjaiWh8xRJIm6gyuW+MG81A=
|
||||
git.rootprojects.org/root/go-gitver v1.1.3/go.mod h1:Rj1v3TBhvdaSphFEqMynUYwAz/4f+wY/+syBTvRrmlI=
|
||||
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 h1:LepdCS8Gf/MVejFIt8lsiexZATdoGVyp5bcyS+rYoUI=
|
||||
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
58
pathman.go
58
pathman.go
|
@ -5,13 +5,24 @@ import (
|
|||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// GitRev is the git commit hash of the build
|
||||
var GitRev = "000000000"
|
||||
|
||||
// GitVersion is the git description converted to semver
|
||||
var GitVersion = "v0.5.2-pre+dirty"
|
||||
|
||||
// GitTimestamp is the timestamp of the latest commit
|
||||
var GitTimestamp = time.Now().Format(time.RFC3339)
|
||||
|
||||
func usage() {
|
||||
fmt.Fprintf(os.Stdout, "Usage: envpath <action> [path]\n")
|
||||
fmt.Fprintf(os.Stdout, "\tex: envpath list\n")
|
||||
fmt.Fprintf(os.Stdout, "\tex: envpath add ~/.local/bin\n")
|
||||
fmt.Fprintf(os.Stdout, "\tex: envpath remove ~/.local/bin\n")
|
||||
fmt.Fprintf(os.Stdout, "Usage: pathman <action> [path]\n")
|
||||
fmt.Fprintf(os.Stdout, "\tex: pathman list\n")
|
||||
fmt.Fprintf(os.Stdout, "\tex: pathman add ~/.local/bin\n")
|
||||
fmt.Fprintf(os.Stdout, "\tex: pathman remove ~/.local/bin\n")
|
||||
fmt.Fprintf(os.Stdout, "\tex: pathman version\n")
|
||||
}
|
||||
|
||||
func main() {
|
||||
|
@ -29,7 +40,7 @@ func main() {
|
|||
}
|
||||
|
||||
action = os.Args[1]
|
||||
if 2 == len(os.Args) {
|
||||
if 3 == len(os.Args) {
|
||||
entry = os.Args[2]
|
||||
}
|
||||
|
||||
|
@ -37,6 +48,7 @@ func main() {
|
|||
// https://github.com/rust-lang-nursery/rustup.rs/issues/686#issuecomment-253982841
|
||||
// exec source $HOME/.profile
|
||||
shell := os.Getenv("SHELL")
|
||||
shell = filepath.Base(shell)
|
||||
switch shell {
|
||||
case "":
|
||||
if strings.HasSuffix(os.Getenv("COMSPEC"), "/cmd.exe") {
|
||||
|
@ -52,15 +64,27 @@ func main() {
|
|||
// warn and try anyway
|
||||
fmt.Fprintf(
|
||||
os.Stderr,
|
||||
"%q isn't a recognized shell. Please open an issue at https://git.rootprojects.org/envpath/issues?q=%s",
|
||||
"%q isn't a recognized shell. Please open an issue at https://git.rootprojects.org/root/pathman/issues?q=%s",
|
||||
shell,
|
||||
shell,
|
||||
)
|
||||
}
|
||||
|
||||
home, _ := os.UserHomeDir()
|
||||
if "" != entry && '~' == entry[0] {
|
||||
// Let windows users not to have to type %USERPROFILE% or \Users\me every time
|
||||
entry = strings.Replace(entry, "~", home, 0)
|
||||
}
|
||||
switch action {
|
||||
default:
|
||||
usage()
|
||||
os.Exit(1)
|
||||
case "version":
|
||||
fmt.Printf("pathman %s (%s) %s\n", GitVersion, GitRev, GitTimestamp)
|
||||
os.Exit(0)
|
||||
return
|
||||
case "list":
|
||||
if 2 == len(os.Args) {
|
||||
if 2 != len(os.Args) {
|
||||
usage()
|
||||
os.Exit(1)
|
||||
}
|
||||
|
@ -91,18 +115,24 @@ func list() {
|
|||
fmt.Println("other PATH entries:\n")
|
||||
// All managed paths
|
||||
pathsmap := map[string]bool{}
|
||||
home, _ := os.UserHomeDir()
|
||||
for i := range managedpaths {
|
||||
// TODO normalize
|
||||
pathsmap[managedpaths[i]] = true
|
||||
}
|
||||
|
||||
// Paths in the environment which are not managed
|
||||
var hasExtras bool
|
||||
envpaths := Paths()
|
||||
for i := range envpaths {
|
||||
paths := Paths()
|
||||
for i := range paths {
|
||||
// TODO normalize
|
||||
path := envpaths[i]
|
||||
if !pathsmap[path] {
|
||||
path := paths[i]
|
||||
path1 := ""
|
||||
path2 := ""
|
||||
if strings.HasPrefix(path, home) {
|
||||
path1 = "$HOME" + strings.TrimPrefix(path, home)
|
||||
path2 = "%USERPROFILE%" + strings.TrimPrefix(path, home)
|
||||
}
|
||||
if !pathsmap[path] && !pathsmap[path1] && !pathsmap[path2] {
|
||||
hasExtras = true
|
||||
fmt.Println("\t" + path)
|
||||
}
|
||||
|
@ -185,7 +215,7 @@ func remove(entry string) {
|
|||
fmt.Fprintf(os.Stderr, "%s", err)
|
||||
}
|
||||
|
||||
msg += " To set the PATH immediately, update the current session:\n\n\t" + Remove(entry) + "\n"
|
||||
msg += " To set the PATH immediately, update the current session:\n\n\t" + Remove(newpaths) + "\n"
|
||||
}
|
||||
|
||||
fmt.Println(msg + "\n")
|
||||
|
@ -224,6 +254,6 @@ func Remove(entries []string) string {
|
|||
return fmt.Sprintf(`export PATH="%s"`, strings.Join(entries, ":"))
|
||||
}
|
||||
|
||||
func isCmdExe() {
|
||||
func isCmdExe() bool {
|
||||
return "" == os.Getenv("SHELL") && strings.Contains(strings.ToLower(os.Getenv("COMSPEC")), "/cmd.exe")
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
// +build windows
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
|
@ -15,7 +15,7 @@ func removePath(p string) (bool, error) {
|
|||
}
|
||||
|
||||
func listPaths() ([]string, error) {
|
||||
return envpath.List()
|
||||
return envpath.Paths()
|
||||
}
|
||||
|
||||
func indexOfPath(cur []string, p string) int {
|
||||
|
|
|
@ -15,7 +15,7 @@ func removePath(p string) (bool, error) {
|
|||
}
|
||||
|
||||
func listPaths() ([]string, error) {
|
||||
return winpath.List()
|
||||
return winpath.Paths()
|
||||
}
|
||||
|
||||
func indexOfPath(cur []string, p string) int {
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
// +build tools
|
||||
|
||||
package tools
|
||||
|
||||
import (
|
||||
_ "git.rootprojects.org/root/go-gitver"
|
||||
)
|
|
@ -50,7 +50,7 @@ func remove(p string) (bool, error) {
|
|||
return false, err
|
||||
}
|
||||
|
||||
index := findMatch(cur, p)
|
||||
index := IndexOf(cur, p)
|
||||
// skip silently, successfully
|
||||
if index < 0 {
|
||||
return false, nil
|
||||
|
|
Loading…
Reference in New Issue